summaryrefslogtreecommitdiffstats
path: root/chrome/browser/ui
diff options
context:
space:
mode:
Diffstat (limited to 'chrome/browser/ui')
-rw-r--r--chrome/browser/ui/browser.cc2
-rw-r--r--chrome/browser/ui/browser_init.cc2
-rw-r--r--chrome/browser/ui/cocoa/DEPS4
-rw-r--r--chrome/browser/ui/cocoa/about_ipc_bridge.h33
-rw-r--r--chrome/browser/ui/cocoa/about_ipc_bridge.mm21
-rw-r--r--chrome/browser/ui/cocoa/about_ipc_controller.h84
-rw-r--r--chrome/browser/ui/cocoa/about_ipc_controller.mm198
-rw-r--r--chrome/browser/ui/cocoa/about_ipc_controller_unittest.mm50
-rw-r--r--chrome/browser/ui/cocoa/about_ipc_dialog.h24
-rw-r--r--chrome/browser/ui/cocoa/about_ipc_dialog.mm21
-rw-r--r--chrome/browser/ui/cocoa/about_window_controller.h69
-rw-r--r--chrome/browser/ui/cocoa/about_window_controller.mm761
-rw-r--r--chrome/browser/ui/cocoa/about_window_controller_unittest.mm137
-rw-r--r--chrome/browser/ui/cocoa/accelerators_cocoa.h41
-rw-r--r--chrome/browser/ui/cocoa/accelerators_cocoa.mm57
-rw-r--r--chrome/browser/ui/cocoa/accelerators_cocoa_unittest.mm28
-rw-r--r--chrome/browser/ui/cocoa/animatable_image.h57
-rw-r--r--chrome/browser/ui/cocoa/animatable_image.mm145
-rw-r--r--chrome/browser/ui/cocoa/animatable_image_unittest.mm46
-rw-r--r--chrome/browser/ui/cocoa/animatable_view.h59
-rw-r--r--chrome/browser/ui/cocoa/animatable_view.mm109
-rw-r--r--chrome/browser/ui/cocoa/animatable_view_unittest.mm48
-rw-r--r--chrome/browser/ui/cocoa/applescript/bookmark_applescript_utils_unittest.h53
-rw-r--r--chrome/browser/ui/cocoa/applescript/bookmark_applescript_utils_unittest.mm62
-rw-r--r--chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript.h70
-rw-r--r--chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript.mm204
-rw-r--r--chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript_unittest.mm200
-rw-r--r--chrome/browser/ui/cocoa/applescript/bookmark_item_applescript.h33
-rw-r--r--chrome/browser/ui/cocoa/applescript/bookmark_item_applescript.mm66
-rw-r--r--chrome/browser/ui/cocoa/applescript/bookmark_item_applescript_unittest.mm45
-rw-r--r--chrome/browser/ui/cocoa/applescript/bookmark_node_applescript.h48
-rw-r--r--chrome/browser/ui/cocoa/applescript/bookmark_node_applescript.mm130
-rw-r--r--chrome/browser/ui/cocoa/applescript/browsercrapplication+applescript.h59
-rw-r--r--chrome/browser/ui/cocoa/applescript/browsercrapplication+applescript.mm136
-rw-r--r--chrome/browser/ui/cocoa/applescript/browsercrapplication+applescript_test.mm107
-rw-r--r--chrome/browser/ui/cocoa/applescript/constants_applescript.h31
-rw-r--r--chrome/browser/ui/cocoa/applescript/constants_applescript.mm25
-rw-r--r--chrome/browser/ui/cocoa/applescript/element_applescript.h37
-rw-r--r--chrome/browser/ui/cocoa/applescript/element_applescript.mm38
-rw-r--r--chrome/browser/ui/cocoa/applescript/error_applescript.h41
-rw-r--r--chrome/browser/ui/cocoa/applescript/error_applescript.mm56
-rw-r--r--chrome/browser/ui/cocoa/applescript/examples/advanced_tab_manipulation.applescript24
-rw-r--r--chrome/browser/ui/cocoa/applescript/examples/app_info.applescript9
-rw-r--r--chrome/browser/ui/cocoa/applescript/examples/bookmark_current_tabs.applescript23
-rw-r--r--chrome/browser/ui/cocoa/applescript/examples/copy_html.applescript16
-rw-r--r--chrome/browser/ui/cocoa/applescript/examples/delete_bookmarks.applescript13
-rw-r--r--chrome/browser/ui/cocoa/applescript/examples/execute_javascript.applescript10
-rw-r--r--chrome/browser/ui/cocoa/applescript/examples/open_tabs_from_bookmark_folder.applescript12
-rw-r--r--chrome/browser/ui/cocoa/applescript/examples/quit_app.applescript8
-rw-r--r--chrome/browser/ui/cocoa/applescript/examples/tab_manipulation.applescript45
-rw-r--r--chrome/browser/ui/cocoa/applescript/examples/tab_navigation.applescript13
-rw-r--r--chrome/browser/ui/cocoa/applescript/examples/window_creation.applescript10
-rw-r--r--chrome/browser/ui/cocoa/applescript/examples/window_operations.applescript22
-rw-r--r--chrome/browser/ui/cocoa/applescript/scripting.sdef304
-rw-r--r--chrome/browser/ui/cocoa/applescript/tab_applescript.h79
-rw-r--r--chrome/browser/ui/cocoa/applescript/tab_applescript.mm296
-rw-r--r--chrome/browser/ui/cocoa/applescript/window_applescript.h81
-rw-r--r--chrome/browser/ui/cocoa/applescript/window_applescript.mm246
-rw-r--r--chrome/browser/ui/cocoa/applescript/window_applescript_test.mm178
-rw-r--r--chrome/browser/ui/cocoa/authorization_util.h67
-rw-r--r--chrome/browser/ui/cocoa/authorization_util.mm184
-rw-r--r--chrome/browser/ui/cocoa/back_forward_menu_controller.h43
-rw-r--r--chrome/browser/ui/cocoa/back_forward_menu_controller.mm102
-rw-r--r--chrome/browser/ui/cocoa/background_gradient_view.h29
-rw-r--r--chrome/browser/ui/cocoa/background_gradient_view.mm81
-rw-r--r--chrome/browser/ui/cocoa/background_gradient_view_unittest.mm47
-rw-r--r--chrome/browser/ui/cocoa/background_tile_view.h23
-rw-r--r--chrome/browser/ui/cocoa/background_tile_view.mm32
-rw-r--r--chrome/browser/ui/cocoa/background_tile_view_unittest.mm37
-rw-r--r--chrome/browser/ui/cocoa/base_bubble_controller.h67
-rw-r--r--chrome/browser/ui/cocoa/base_bubble_controller.mm201
-rw-r--r--chrome/browser/ui/cocoa/base_view.h45
-rw-r--r--chrome/browser/ui/cocoa/base_view.mm147
-rw-r--r--chrome/browser/ui/cocoa/base_view_unittest.mm48
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h46
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.mm88
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller_unittest.mm82
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h60
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.mm82
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge_unittest.mm135
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h38
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h399
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.mm2497
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller_unittest.mm2169
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h31
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.mm22
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell_unittest.mm24
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h182
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.mm1459
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller_unittest.mm1552
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h78
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.mm171
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state_unittest.mm77
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h29
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.mm204
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view_unittest.mm211
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h34
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.mm136
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window_unittest.mm49
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_state.h62
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h44
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.mm135
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view_unittest.mm191
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.h57
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.mm81
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h41
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.mm259
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view_unittest.mm215
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.h81
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.mm428
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller_unittest.mm490
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_button.h243
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_button.mm238
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h65
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.mm246
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell_unittest.mm183
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_button_unittest.mm174
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.h30
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.mm43
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h171
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.mm604
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller_unittest.mm235
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h36
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.mm143
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller_unittest.mm423
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h50
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.mm118
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target_unittest.mm125
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h20
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu.mm22
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h123
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.mm253
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge_unittest.mm317
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h46
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.mm98
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller_unittest.mm66
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu_unittest.mm29
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa.h116
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa_unittest.mm68
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h64
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.mm123
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller_unittest.mm172
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h35
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.mm23
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell_unittest.mm43
-rw-r--r--chrome/browser/ui/cocoa/browser_command_executor.h16
-rw-r--r--chrome/browser/ui/cocoa/browser_frame_view.h65
-rw-r--r--chrome/browser/ui/cocoa/browser_frame_view.mm399
-rw-r--r--chrome/browser/ui/cocoa/browser_frame_view_unittest.mm48
-rw-r--r--chrome/browser/ui/cocoa/browser_test_helper.h92
-rw-r--r--chrome/browser/ui/cocoa/browser_window_cocoa.h143
-rw-r--r--chrome/browser/ui/cocoa/browser_window_cocoa.mm638
-rw-r--r--chrome/browser/ui/cocoa/browser_window_cocoa_unittest.mm120
-rw-r--r--chrome/browser/ui/cocoa/browser_window_controller.h397
-rw-r--r--chrome/browser/ui/cocoa/browser_window_controller.mm2059
-rw-r--r--chrome/browser/ui/cocoa/browser_window_controller_private.h119
-rw-r--r--chrome/browser/ui/cocoa/browser_window_controller_private.mm509
-rw-r--r--chrome/browser/ui/cocoa/browser_window_controller_unittest.mm670
-rw-r--r--chrome/browser/ui/cocoa/browser_window_factory.mm32
-rw-r--r--chrome/browser/ui/cocoa/bubble_view.h66
-rw-r--r--chrome/browser/ui/cocoa/bubble_view.mm120
-rw-r--r--chrome/browser/ui/cocoa/bubble_view_unittest.mm58
-rw-r--r--chrome/browser/ui/cocoa/bug_report_window_controller.h112
-rw-r--r--chrome/browser/ui/cocoa/bug_report_window_controller.mm231
-rw-r--r--chrome/browser/ui/cocoa/bug_report_window_controller_unittest.mm78
-rw-r--r--chrome/browser/ui/cocoa/certificate_viewer.mm45
-rw-r--r--chrome/browser/ui/cocoa/chrome_browser_window.h28
-rw-r--r--chrome/browser/ui/cocoa/chrome_browser_window.mm52
-rw-r--r--chrome/browser/ui/cocoa/chrome_browser_window_unittest.mm45
-rw-r--r--chrome/browser/ui/cocoa/chrome_event_processing_window.h49
-rw-r--r--chrome/browser/ui/cocoa/chrome_event_processing_window.mm164
-rw-r--r--chrome/browser/ui/cocoa/chrome_event_processing_window_unittest.mm104
-rw-r--r--chrome/browser/ui/cocoa/clear_browsing_data_controller.h87
-rw-r--r--chrome/browser/ui/cocoa/clear_browsing_data_controller.mm264
-rw-r--r--chrome/browser/ui/cocoa/clear_browsing_data_controller_unittest.mm149
-rw-r--r--chrome/browser/ui/cocoa/clickhold_button_cell.h48
-rw-r--r--chrome/browser/ui/cocoa/clickhold_button_cell.mm190
-rw-r--r--chrome/browser/ui/cocoa/clickhold_button_cell_unittest.mm51
-rw-r--r--chrome/browser/ui/cocoa/cocoa_test_helper.h153
-rw-r--r--chrome/browser/ui/cocoa/cocoa_test_helper.mm205
-rw-r--r--chrome/browser/ui/cocoa/collected_cookies_mac.h123
-rw-r--r--chrome/browser/ui/cocoa/collected_cookies_mac.mm499
-rw-r--r--chrome/browser/ui/cocoa/collected_cookies_mac_unittest.mm38
-rw-r--r--chrome/browser/ui/cocoa/command_observer_bridge.h47
-rw-r--r--chrome/browser/ui/cocoa/command_observer_bridge.mm28
-rw-r--r--chrome/browser/ui/cocoa/command_observer_bridge_unittest.mm89
-rw-r--r--chrome/browser/ui/cocoa/confirm_quit_panel_controller.h25
-rw-r--r--chrome/browser/ui/cocoa/confirm_quit_panel_controller.mm85
-rw-r--r--chrome/browser/ui/cocoa/confirm_quit_panel_controller_unittest.mm27
-rw-r--r--chrome/browser/ui/cocoa/constrained_html_delegate_mac.mm153
-rw-r--r--chrome/browser/ui/cocoa/constrained_window_mac.h165
-rw-r--r--chrome/browser/ui/cocoa/constrained_window_mac.mm104
-rw-r--r--chrome/browser/ui/cocoa/content_exceptions_window_controller.h74
-rw-r--r--chrome/browser/ui/cocoa/content_exceptions_window_controller.mm490
-rw-r--r--chrome/browser/ui/cocoa/content_exceptions_window_controller_unittest.mm252
-rw-r--r--chrome/browser/ui/cocoa/content_setting_bubble_cocoa.h67
-rw-r--r--chrome/browser/ui/cocoa/content_setting_bubble_cocoa.mm487
-rw-r--r--chrome/browser/ui/cocoa/content_setting_bubble_cocoa_unittest.mm63
-rw-r--r--chrome/browser/ui/cocoa/content_settings_dialog_controller.h102
-rw-r--r--chrome/browser/ui/cocoa/content_settings_dialog_controller.mm647
-rw-r--r--chrome/browser/ui/cocoa/content_settings_dialog_controller_unittest.mm289
-rw-r--r--chrome/browser/ui/cocoa/cookie_details.h224
-rw-r--r--chrome/browser/ui/cocoa/cookie_details.mm299
-rw-r--r--chrome/browser/ui/cocoa/cookie_details_unittest.mm247
-rw-r--r--chrome/browser/ui/cocoa/cookie_details_view_controller.h56
-rw-r--r--chrome/browser/ui/cocoa/cookie_details_view_controller.mm110
-rw-r--r--chrome/browser/ui/cocoa/cookie_details_view_controller_unittest.mm88
-rw-r--r--chrome/browser/ui/cocoa/cookie_tree_node.h37
-rw-r--r--chrome/browser/ui/cocoa/cookie_tree_node.mm73
-rw-r--r--chrome/browser/ui/cocoa/cookies_window_controller.h146
-rw-r--r--chrome/browser/ui/cocoa/cookies_window_controller.mm448
-rw-r--r--chrome/browser/ui/cocoa/cookies_window_controller_unittest.mm687
-rw-r--r--chrome/browser/ui/cocoa/custom_home_pages_model.h91
-rw-r--r--chrome/browser/ui/cocoa/custom_home_pages_model.mm140
-rw-r--r--chrome/browser/ui/cocoa/custom_home_pages_model_unittest.mm196
-rw-r--r--chrome/browser/ui/cocoa/delayedmenu_button.h32
-rw-r--r--chrome/browser/ui/cocoa/delayedmenu_button.mm137
-rw-r--r--chrome/browser/ui/cocoa/delayedmenu_button_unittest.mm62
-rw-r--r--chrome/browser/ui/cocoa/dev_tools_controller.h51
-rw-r--r--chrome/browser/ui/cocoa/dev_tools_controller.mm164
-rw-r--r--chrome/browser/ui/cocoa/dock_icon.h30
-rw-r--r--chrome/browser/ui/cocoa/dock_icon.mm224
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_button.h27
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_button.mm50
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_button_unittest.mm21
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_cell.h61
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_cell.mm708
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_controller.h105
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_controller.mm398
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_mac.h63
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_mac.mm96
-rw-r--r--chrome/browser/ui/cocoa/download/download_shelf_controller.h95
-rw-r--r--chrome/browser/ui/cocoa/download/download_shelf_controller.mm327
-rw-r--r--chrome/browser/ui/cocoa/download/download_shelf_mac.h43
-rw-r--r--chrome/browser/ui/cocoa/download/download_shelf_mac.mm40
-rw-r--r--chrome/browser/ui/cocoa/download/download_shelf_mac_unittest.mm91
-rw-r--r--chrome/browser/ui/cocoa/download/download_shelf_view.h20
-rw-r--r--chrome/browser/ui/cocoa/download/download_shelf_view.mm71
-rw-r--r--chrome/browser/ui/cocoa/download/download_shelf_view_unittest.mm23
-rw-r--r--chrome/browser/ui/cocoa/download/download_started_animation_mac.mm195
-rw-r--r--chrome/browser/ui/cocoa/download/download_util_mac.h25
-rw-r--r--chrome/browser/ui/cocoa/download/download_util_mac.mm83
-rw-r--r--chrome/browser/ui/cocoa/download/download_util_mac_unittest.mm58
-rw-r--r--chrome/browser/ui/cocoa/draggable_button.h33
-rw-r--r--chrome/browser/ui/cocoa/draggable_button.mm150
-rw-r--r--chrome/browser/ui/cocoa/draggable_button_unittest.mm137
-rw-r--r--chrome/browser/ui/cocoa/edit_search_engine_cocoa_controller.h53
-rw-r--r--chrome/browser/ui/cocoa/edit_search_engine_cocoa_controller.mm187
-rw-r--r--chrome/browser/ui/cocoa/edit_search_engine_cocoa_controller_unittest.mm233
-rw-r--r--chrome/browser/ui/cocoa/encoding_menu_controller_delegate_mac.h24
-rw-r--r--chrome/browser/ui/cocoa/encoding_menu_controller_delegate_mac.mm60
-rw-r--r--chrome/browser/ui/cocoa/event_utils.h30
-rw-r--r--chrome/browser/ui/cocoa/event_utils.mm21
-rw-r--r--chrome/browser/ui/cocoa/event_utils_unittest.mm61
-rw-r--r--chrome/browser/ui/cocoa/extension_install_prompt.mm51
-rw-r--r--chrome/browser/ui/cocoa/extension_installed_bubble_bridge.h28
-rw-r--r--chrome/browser/ui/cocoa/extension_installed_bubble_bridge.mm25
-rw-r--r--chrome/browser/ui/cocoa/extension_installed_bubble_controller.h112
-rw-r--r--chrome/browser/ui/cocoa/extension_installed_bubble_controller.mm374
-rw-r--r--chrome/browser/ui/cocoa/extension_installed_bubble_controller_unittest.mm202
-rw-r--r--chrome/browser/ui/cocoa/extension_view_mac.h88
-rw-r--r--chrome/browser/ui/cocoa/extension_view_mac.mm112
-rw-r--r--chrome/browser/ui/cocoa/extensions/browser_action_button.h98
-rw-r--r--chrome/browser/ui/cocoa/extensions/browser_action_button.mm333
-rw-r--r--chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h84
-rw-r--r--chrome/browser/ui/cocoa/extensions/browser_actions_container_view.mm192
-rw-r--r--chrome/browser/ui/cocoa/extensions/browser_actions_container_view_unittest.mm52
-rw-r--r--chrome/browser/ui/cocoa/extensions/browser_actions_controller.h117
-rw-r--r--chrome/browser/ui/cocoa/extensions/browser_actions_controller.mm863
-rw-r--r--chrome/browser/ui/cocoa/extensions/chevron_menu_button.h19
-rw-r--r--chrome/browser/ui/cocoa/extensions/chevron_menu_button.mm15
-rw-r--r--chrome/browser/ui/cocoa/extensions/chevron_menu_button_cell.h19
-rw-r--r--chrome/browser/ui/cocoa/extensions/chevron_menu_button_cell.mm47
-rw-r--r--chrome/browser/ui/cocoa/extensions/chevron_menu_button_unittest.mm50
-rw-r--r--chrome/browser/ui/cocoa/extensions/extension_action_context_menu.h62
-rw-r--r--chrome/browser/ui/cocoa/extensions/extension_action_context_menu.mm278
-rw-r--r--chrome/browser/ui/cocoa/extensions/extension_infobar_controller.h41
-rw-r--r--chrome/browser/ui/cocoa/extensions/extension_infobar_controller.mm266
-rw-r--r--chrome/browser/ui/cocoa/extensions/extension_install_prompt_controller.h62
-rw-r--r--chrome/browser/ui/cocoa/extensions/extension_install_prompt_controller.mm217
-rw-r--r--chrome/browser/ui/cocoa/extensions/extension_install_prompt_controller_unittest.mm286
-rw-r--r--chrome/browser/ui/cocoa/extensions/extension_popup_controller.h99
-rw-r--r--chrome/browser/ui/cocoa/extensions/extension_popup_controller.mm338
-rw-r--r--chrome/browser/ui/cocoa/extensions/extension_popup_controller_unittest.mm89
-rw-r--r--chrome/browser/ui/cocoa/external_protocol_dialog.h19
-rw-r--r--chrome/browser/ui/cocoa/external_protocol_dialog.mm152
-rw-r--r--chrome/browser/ui/cocoa/fast_resize_view.h29
-rw-r--r--chrome/browser/ui/cocoa/fast_resize_view.mm65
-rw-r--r--chrome/browser/ui/cocoa/fast_resize_view_unittest.mm60
-rw-r--r--chrome/browser/ui/cocoa/file_metadata.h29
-rw-r--r--chrome/browser/ui/cocoa/file_metadata.mm167
-rw-r--r--chrome/browser/ui/cocoa/find_bar_bridge.h95
-rw-r--r--chrome/browser/ui/cocoa/find_bar_bridge.mm96
-rw-r--r--chrome/browser/ui/cocoa/find_bar_bridge_unittest.mm30
-rw-r--r--chrome/browser/ui/cocoa/find_bar_cocoa_controller.h78
-rw-r--r--chrome/browser/ui/cocoa/find_bar_cocoa_controller.mm384
-rw-r--r--chrome/browser/ui/cocoa/find_bar_cocoa_controller_unittest.mm138
-rw-r--r--chrome/browser/ui/cocoa/find_bar_text_field.h21
-rw-r--r--chrome/browser/ui/cocoa/find_bar_text_field.mm40
-rw-r--r--chrome/browser/ui/cocoa/find_bar_text_field_cell.h24
-rw-r--r--chrome/browser/ui/cocoa/find_bar_text_field_cell.mm119
-rw-r--r--chrome/browser/ui/cocoa/find_bar_text_field_cell_unittest.mm135
-rw-r--r--chrome/browser/ui/cocoa/find_bar_text_field_unittest.mm92
-rw-r--r--chrome/browser/ui/cocoa/find_bar_view.h19
-rw-r--r--chrome/browser/ui/cocoa/find_bar_view.mm131
-rw-r--r--chrome/browser/ui/cocoa/find_bar_view_unittest.mm90
-rw-r--r--chrome/browser/ui/cocoa/find_pasteboard.h58
-rw-r--r--chrome/browser/ui/cocoa/find_pasteboard.mm82
-rw-r--r--chrome/browser/ui/cocoa/find_pasteboard_unittest.mm115
-rw-r--r--chrome/browser/ui/cocoa/first_run_bubble_controller.h24
-rw-r--r--chrome/browser/ui/cocoa/first_run_bubble_controller.mm84
-rw-r--r--chrome/browser/ui/cocoa/first_run_bubble_controller_unittest.mm44
-rw-r--r--chrome/browser/ui/cocoa/first_run_dialog.h36
-rw-r--r--chrome/browser/ui/cocoa/first_run_dialog.mm195
-rw-r--r--chrome/browser/ui/cocoa/floating_bar_backing_view.h15
-rw-r--r--chrome/browser/ui/cocoa/floating_bar_backing_view.mm51
-rw-r--r--chrome/browser/ui/cocoa/floating_bar_backing_view_unittest.mm26
-rw-r--r--chrome/browser/ui/cocoa/focus_tracker.h28
-rw-r--r--chrome/browser/ui/cocoa/focus_tracker.mm46
-rw-r--r--chrome/browser/ui/cocoa/focus_tracker_unittest.mm90
-rw-r--r--chrome/browser/ui/cocoa/font_language_settings_controller.h94
-rw-r--r--chrome/browser/ui/cocoa/font_language_settings_controller.mm280
-rw-r--r--chrome/browser/ui/cocoa/font_language_settings_controller_unittest.mm91
-rw-r--r--chrome/browser/ui/cocoa/framed_browser_window.h65
-rw-r--r--chrome/browser/ui/cocoa/framed_browser_window.mm350
-rw-r--r--chrome/browser/ui/cocoa/framed_browser_window_unittest.mm184
-rw-r--r--chrome/browser/ui/cocoa/fullscreen_controller.h122
-rw-r--r--chrome/browser/ui/cocoa/fullscreen_controller.mm633
-rw-r--r--chrome/browser/ui/cocoa/fullscreen_window.h19
-rw-r--r--chrome/browser/ui/cocoa/fullscreen_window.mm100
-rw-r--r--chrome/browser/ui/cocoa/fullscreen_window_unittest.mm47
-rw-r--r--chrome/browser/ui/cocoa/gradient_button_cell.h120
-rw-r--r--chrome/browser/ui/cocoa/gradient_button_cell.mm719
-rw-r--r--chrome/browser/ui/cocoa/gradient_button_cell_unittest.mm112
-rw-r--r--chrome/browser/ui/cocoa/history_menu_bridge.h232
-rw-r--r--chrome/browser/ui/cocoa/history_menu_bridge.mm470
-rw-r--r--chrome/browser/ui/cocoa/history_menu_bridge_unittest.mm386
-rw-r--r--chrome/browser/ui/cocoa/history_menu_cocoa_controller.h32
-rw-r--r--chrome/browser/ui/cocoa/history_menu_cocoa_controller.mm58
-rw-r--r--chrome/browser/ui/cocoa/history_menu_cocoa_controller_unittest.mm91
-rw-r--r--chrome/browser/ui/cocoa/hover_button.h35
-rw-r--r--chrome/browser/ui/cocoa/hover_button.mm97
-rw-r--r--chrome/browser/ui/cocoa/hover_close_button.h26
-rw-r--r--chrome/browser/ui/cocoa/hover_close_button.mm108
-rw-r--r--chrome/browser/ui/cocoa/hover_image_button.h40
-rw-r--r--chrome/browser/ui/cocoa/hover_image_button.mm52
-rw-r--r--chrome/browser/ui/cocoa/hover_image_button_unittest.mm67
-rw-r--r--chrome/browser/ui/cocoa/html_dialog_window_controller.h55
-rw-r--r--chrome/browser/ui/cocoa/html_dialog_window_controller.mm293
-rw-r--r--chrome/browser/ui/cocoa/html_dialog_window_controller_cppsafe.h32
-rw-r--r--chrome/browser/ui/cocoa/html_dialog_window_controller_unittest.mm94
-rw-r--r--chrome/browser/ui/cocoa/hung_renderer_controller.h76
-rw-r--r--chrome/browser/ui/cocoa/hung_renderer_controller.mm203
-rw-r--r--chrome/browser/ui/cocoa/hung_renderer_controller_unittest.mm51
-rw-r--r--chrome/browser/ui/cocoa/hyperlink_button_cell.h25
-rw-r--r--chrome/browser/ui/cocoa/hyperlink_button_cell.mm116
-rw-r--r--chrome/browser/ui/cocoa/hyperlink_button_cell_unittest.mm75
-rw-r--r--chrome/browser/ui/cocoa/image_utils.h26
-rw-r--r--chrome/browser/ui/cocoa/image_utils.mm37
-rw-r--r--chrome/browser/ui/cocoa/image_utils_unittest.mm138
-rw-r--r--chrome/browser/ui/cocoa/import_progress_dialog.h102
-rw-r--r--chrome/browser/ui/cocoa/import_progress_dialog.mm192
-rw-r--r--chrome/browser/ui/cocoa/import_settings_dialog.h98
-rw-r--r--chrome/browser/ui/cocoa/import_settings_dialog.mm245
-rw-r--r--chrome/browser/ui/cocoa/import_settings_dialog_unittest.mm130
-rw-r--r--chrome/browser/ui/cocoa/importer_lock_dialog.h21
-rw-r--r--chrome/browser/ui/cocoa/importer_lock_dialog.mm35
-rw-r--r--chrome/browser/ui/cocoa/info_bubble_view.h50
-rw-r--r--chrome/browser/ui/cocoa/info_bubble_view.mm103
-rw-r--r--chrome/browser/ui/cocoa/info_bubble_view_unittest.mm26
-rw-r--r--chrome/browser/ui/cocoa/info_bubble_window.h32
-rw-r--r--chrome/browser/ui/cocoa/info_bubble_window.mm222
-rw-r--r--chrome/browser/ui/cocoa/info_bubble_window_unittest.mm22
-rw-r--r--chrome/browser/ui/cocoa/infobar.h48
-rw-r--r--chrome/browser/ui/cocoa/infobar_container_controller.h113
-rw-r--r--chrome/browser/ui/cocoa/infobar_container_controller.mm228
-rw-r--r--chrome/browser/ui/cocoa/infobar_container_controller_unittest.mm95
-rw-r--r--chrome/browser/ui/cocoa/infobar_controller.h106
-rw-r--r--chrome/browser/ui/cocoa/infobar_controller.mm534
-rw-r--r--chrome/browser/ui/cocoa/infobar_controller_unittest.mm284
-rw-r--r--chrome/browser/ui/cocoa/infobar_gradient_view.h19
-rw-r--r--chrome/browser/ui/cocoa/infobar_gradient_view.mm70
-rw-r--r--chrome/browser/ui/cocoa/infobar_gradient_view_unittest.mm32
-rw-r--r--chrome/browser/ui/cocoa/infobar_test_helper.h165
-rwxr-xr-xchrome/browser/ui/cocoa/install.sh123
-rw-r--r--chrome/browser/ui/cocoa/install_from_dmg.h15
-rw-r--r--chrome/browser/ui/cocoa/install_from_dmg.mm438
-rw-r--r--chrome/browser/ui/cocoa/instant_confirm_window_controller.h43
-rw-r--r--chrome/browser/ui/cocoa/instant_confirm_window_controller.mm76
-rw-r--r--chrome/browser/ui/cocoa/instant_confirm_window_controller_unittest.mm36
-rw-r--r--chrome/browser/ui/cocoa/js_modal_dialog_cocoa.h48
-rw-r--r--chrome/browser/ui/cocoa/js_modal_dialog_cocoa.mm219
-rw-r--r--chrome/browser/ui/cocoa/keystone_glue.h209
-rw-r--r--chrome/browser/ui/cocoa/keystone_glue.mm959
-rw-r--r--chrome/browser/ui/cocoa/keystone_glue_unittest.mm184
-rw-r--r--chrome/browser/ui/cocoa/keystone_infobar.h24
-rw-r--r--chrome/browser/ui/cocoa/keystone_infobar.mm212
-rwxr-xr-xchrome/browser/ui/cocoa/keystone_promote_postflight.sh55
-rwxr-xr-xchrome/browser/ui/cocoa/keystone_promote_preflight.sh97
-rw-r--r--chrome/browser/ui/cocoa/keyword_editor_cocoa_controller.h117
-rw-r--r--chrome/browser/ui/cocoa/keyword_editor_cocoa_controller.mm422
-rw-r--r--chrome/browser/ui/cocoa/keyword_editor_cocoa_controller_unittest.mm227
-rw-r--r--chrome/browser/ui/cocoa/l10n_util.h32
-rw-r--r--chrome/browser/ui/cocoa/l10n_util.mm78
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h144
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.mm385
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h76
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.mm402
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell_unittest.mm300
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h56
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.mm371
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor_unittest.mm297
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest.mm792
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.h58
-rw-r--r--chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.mm29
-rw-r--r--chrome/browser/ui/cocoa/location_bar/bubble_decoration.h67
-rw-r--r--chrome/browser/ui/cocoa/location_bar/bubble_decoration.mm158
-rw-r--r--chrome/browser/ui/cocoa/location_bar/content_setting_decoration.h55
-rw-r--r--chrome/browser/ui/cocoa/location_bar/content_setting_decoration.mm109
-rw-r--r--chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.h59
-rw-r--r--chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.mm117
-rw-r--r--chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration_unittest.mm55
-rw-r--r--chrome/browser/ui/cocoa/location_bar/image_decoration.h36
-rw-r--r--chrome/browser/ui/cocoa/location_bar/image_decoration.mm54
-rw-r--r--chrome/browser/ui/cocoa/location_bar/image_decoration_unittest.mm55
-rw-r--r--chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.h43
-rw-r--r--chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.mm31
-rw-r--r--chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller_unittest.mm62
-rw-r--r--chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.h16
-rw-r--r--chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.mm54
-rw-r--r--chrome/browser/ui/cocoa/location_bar/instant_opt_in_view_unittest.mm26
-rw-r--r--chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.h47
-rw-r--r--chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.mm160
-rw-r--r--chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration_unittest.mm57
-rw-r--r--chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h89
-rw-r--r--chrome/browser/ui/cocoa/location_bar/location_bar_decoration.mm18
-rw-r--r--chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h237
-rw-r--r--chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.mm690
-rw-r--r--chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h46
-rw-r--r--chrome/browser/ui/cocoa/location_bar/location_icon_decoration.mm72
-rw-r--r--chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.h17
-rw-r--r--chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.mm43
-rw-r--r--chrome/browser/ui/cocoa/location_bar/omnibox_popup_view_unittest.mm68
-rw-r--r--chrome/browser/ui/cocoa/location_bar/page_action_decoration.h119
-rw-r--r--chrome/browser/ui/cocoa/location_bar/page_action_decoration.mm251
-rw-r--r--chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.h42
-rw-r--r--chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.mm73
-rw-r--r--chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration_unittest.mm64
-rw-r--r--chrome/browser/ui/cocoa/location_bar/star_decoration.h44
-rw-r--r--chrome/browser/ui/cocoa/location_bar/star_decoration.mm53
-rw-r--r--chrome/browser/ui/cocoa/menu_button.h32
-rw-r--r--chrome/browser/ui/cocoa/menu_button.mm122
-rw-r--r--chrome/browser/ui/cocoa/menu_button_unittest.mm50
-rw-r--r--chrome/browser/ui/cocoa/menu_controller.h67
-rw-r--r--chrome/browser/ui/cocoa/menu_controller.mm185
-rw-r--r--chrome/browser/ui/cocoa/menu_controller_unittest.mm197
-rw-r--r--chrome/browser/ui/cocoa/menu_tracked_button.h43
-rw-r--r--chrome/browser/ui/cocoa/menu_tracked_button.mm118
-rw-r--r--chrome/browser/ui/cocoa/menu_tracked_button_unittest.mm117
-rw-r--r--chrome/browser/ui/cocoa/menu_tracked_root_view.h25
-rw-r--r--chrome/browser/ui/cocoa/menu_tracked_root_view.mm15
-rw-r--r--chrome/browser/ui/cocoa/menu_tracked_root_view_unittest.mm45
-rw-r--r--chrome/browser/ui/cocoa/multi_key_equivalent_button.h36
-rw-r--r--chrome/browser/ui/cocoa/multi_key_equivalent_button.mm33
-rw-r--r--chrome/browser/ui/cocoa/new_tab_button.h28
-rw-r--r--chrome/browser/ui/cocoa/new_tab_button.mm42
-rw-r--r--chrome/browser/ui/cocoa/notifications/balloon_controller.h98
-rw-r--r--chrome/browser/ui/cocoa/notifications/balloon_controller.mm241
-rw-r--r--chrome/browser/ui/cocoa/notifications/balloon_controller_unittest.mm112
-rw-r--r--chrome/browser/ui/cocoa/notifications/balloon_view.h28
-rw-r--r--chrome/browser/ui/cocoa/notifications/balloon_view.mm84
-rw-r--r--chrome/browser/ui/cocoa/notifications/balloon_view_bridge.h40
-rw-r--r--chrome/browser/ui/cocoa/notifications/balloon_view_bridge.mm48
-rw-r--r--chrome/browser/ui/cocoa/notifications/balloon_view_host_mac.h42
-rw-r--r--chrome/browser/ui/cocoa/notifications/balloon_view_host_mac.mm39
-rw-r--r--chrome/browser/ui/cocoa/nsimage_cache_unittest.mm75
-rw-r--r--chrome/browser/ui/cocoa/nsmenuitem_additions.h19
-rw-r--r--chrome/browser/ui/cocoa/nsmenuitem_additions.mm103
-rw-r--r--chrome/browser/ui/cocoa/nsmenuitem_additions_unittest.mm351
-rw-r--r--chrome/browser/ui/cocoa/nswindow_additions.h25
-rw-r--r--chrome/browser/ui/cocoa/nswindow_additions.mm104
-rw-r--r--chrome/browser/ui/cocoa/objc_method_swizzle.h28
-rw-r--r--chrome/browser/ui/cocoa/objc_method_swizzle.mm59
-rw-r--r--chrome/browser/ui/cocoa/objc_method_swizzle_unittest.mm76
-rw-r--r--chrome/browser/ui/cocoa/objc_zombie.h39
-rw-r--r--chrome/browser/ui/cocoa/objc_zombie.mm414
-rw-r--r--chrome/browser/ui/cocoa/page_info_bubble_controller.h47
-rw-r--r--chrome/browser/ui/cocoa/page_info_bubble_controller.mm461
-rw-r--r--chrome/browser/ui/cocoa/page_info_bubble_controller_unittest.mm210
-rw-r--r--chrome/browser/ui/cocoa/preferences_window_controller.h241
-rw-r--r--chrome/browser/ui/cocoa/preferences_window_controller.mm2184
-rw-r--r--chrome/browser/ui/cocoa/preferences_window_controller_unittest.mm240
-rw-r--r--chrome/browser/ui/cocoa/previewable_contents_controller.h47
-rw-r--r--chrome/browser/ui/cocoa/previewable_contents_controller.mm52
-rw-r--r--chrome/browser/ui/cocoa/previewable_contents_controller_unittest.mm34
-rw-r--r--chrome/browser/ui/cocoa/reload_button.h50
-rw-r--r--chrome/browser/ui/cocoa/reload_button.mm168
-rw-r--r--chrome/browser/ui/cocoa/reload_button_unittest.mm259
-rw-r--r--chrome/browser/ui/cocoa/repost_form_warning_mac.h40
-rw-r--r--chrome/browser/ui/cocoa/repost_form_warning_mac.mm82
-rw-r--r--chrome/browser/ui/cocoa/restart_browser.h22
-rw-r--r--chrome/browser/ui/cocoa/restart_browser.mm86
-rw-r--r--chrome/browser/ui/cocoa/rwhvm_editcommand_helper.h72
-rw-r--r--chrome/browser/ui/cocoa/rwhvm_editcommand_helper.mm227
-rw-r--r--chrome/browser/ui/cocoa/rwhvm_editcommand_helper_unittest.mm172
-rw-r--r--chrome/browser/ui/cocoa/sad_tab_controller.h33
-rw-r--r--chrome/browser/ui/cocoa/sad_tab_controller.mm48
-rw-r--r--chrome/browser/ui/cocoa/sad_tab_controller_unittest.mm113
-rw-r--r--chrome/browser/ui/cocoa/sad_tab_view.h36
-rw-r--r--chrome/browser/ui/cocoa/sad_tab_view.mm127
-rw-r--r--chrome/browser/ui/cocoa/sad_tab_view_unittest.mm25
-rw-r--r--chrome/browser/ui/cocoa/scoped_authorizationref.h80
-rw-r--r--chrome/browser/ui/cocoa/search_engine_dialog_controller.h46
-rw-r--r--chrome/browser/ui/cocoa/search_engine_dialog_controller.mm285
-rw-r--r--chrome/browser/ui/cocoa/search_engine_list_model.h48
-rw-r--r--chrome/browser/ui/cocoa/search_engine_list_model.mm136
-rw-r--r--chrome/browser/ui/cocoa/search_engine_list_model_unittest.mm152
-rw-r--r--chrome/browser/ui/cocoa/shell_dialogs_mac.mm417
-rw-r--r--chrome/browser/ui/cocoa/side_tab_strip_controller.h19
-rw-r--r--chrome/browser/ui/cocoa/side_tab_strip_controller.mm33
-rw-r--r--chrome/browser/ui/cocoa/side_tab_strip_view.h15
-rw-r--r--chrome/browser/ui/cocoa/side_tab_strip_view.mm43
-rw-r--r--chrome/browser/ui/cocoa/side_tab_strip_view_unittest.mm30
-rw-r--r--chrome/browser/ui/cocoa/sidebar_controller.h51
-rw-r--r--chrome/browser/ui/cocoa/sidebar_controller.mm179
-rw-r--r--chrome/browser/ui/cocoa/simple_content_exceptions_window_controller.h38
-rw-r--r--chrome/browser/ui/cocoa/simple_content_exceptions_window_controller.mm125
-rw-r--r--chrome/browser/ui/cocoa/simple_content_exceptions_window_controller_unittest.mm94
-rw-r--r--chrome/browser/ui/cocoa/speech_input_window_controller.h57
-rw-r--r--chrome/browser/ui/cocoa/speech_input_window_controller.mm188
-rw-r--r--chrome/browser/ui/cocoa/ssl_client_certificate_selector.mm195
-rw-r--r--chrome/browser/ui/cocoa/status_bubble_mac.h172
-rw-r--r--chrome/browser/ui/cocoa/status_bubble_mac.mm705
-rw-r--r--chrome/browser/ui/cocoa/status_bubble_mac_unittest.mm584
-rw-r--r--chrome/browser/ui/cocoa/status_icons/status_icon_mac.h44
-rw-r--r--chrome/browser/ui/cocoa/status_icons/status_icon_mac.mm82
-rw-r--r--chrome/browser/ui/cocoa/status_icons/status_icon_mac_unittest.mm30
-rw-r--r--chrome/browser/ui/cocoa/status_icons/status_tray_mac.h24
-rw-r--r--chrome/browser/ui/cocoa/status_icons/status_tray_mac.mm18
-rw-r--r--chrome/browser/ui/cocoa/styled_text_field.h29
-rw-r--r--chrome/browser/ui/cocoa/styled_text_field.mm61
-rw-r--r--chrome/browser/ui/cocoa/styled_text_field_cell.h58
-rw-r--r--chrome/browser/ui/cocoa/styled_text_field_cell.mm217
-rw-r--r--chrome/browser/ui/cocoa/styled_text_field_cell_unittest.mm93
-rw-r--r--chrome/browser/ui/cocoa/styled_text_field_test_helper.h16
-rw-r--r--chrome/browser/ui/cocoa/styled_text_field_test_helper.mm18
-rw-r--r--chrome/browser/ui/cocoa/styled_text_field_unittest.mm198
-rw-r--r--chrome/browser/ui/cocoa/tab_contents_controller.h75
-rw-r--r--chrome/browser/ui/cocoa/tab_contents_controller.mm212
-rw-r--r--chrome/browser/ui/cocoa/tab_controller.h113
-rw-r--r--chrome/browser/ui/cocoa/tab_controller.mm313
-rw-r--r--chrome/browser/ui/cocoa/tab_controller_target.h27
-rw-r--r--chrome/browser/ui/cocoa/tab_controller_unittest.mm268
-rw-r--r--chrome/browser/ui/cocoa/tab_strip_controller.h259
-rw-r--r--chrome/browser/ui/cocoa/tab_strip_controller.mm1879
-rw-r--r--chrome/browser/ui/cocoa/tab_strip_controller_unittest.mm177
-rw-r--r--chrome/browser/ui/cocoa/tab_strip_model_observer_bridge.h85
-rw-r--r--chrome/browser/ui/cocoa/tab_strip_model_observer_bridge.mm118
-rw-r--r--chrome/browser/ui/cocoa/tab_strip_view.h48
-rw-r--r--chrome/browser/ui/cocoa/tab_strip_view.mm211
-rw-r--r--chrome/browser/ui/cocoa/tab_strip_view_unittest.mm30
-rw-r--r--chrome/browser/ui/cocoa/tab_view.h134
-rw-r--r--chrome/browser/ui/cocoa/tab_view.mm1057
-rw-r--r--chrome/browser/ui/cocoa/tab_view_picker_table.h29
-rw-r--r--chrome/browser/ui/cocoa/tab_view_picker_table.mm193
-rw-r--r--chrome/browser/ui/cocoa/tab_view_picker_table_unittest.mm138
-rw-r--r--chrome/browser/ui/cocoa/tab_view_unittest.mm60
-rw-r--r--chrome/browser/ui/cocoa/tab_window_controller.h177
-rw-r--r--chrome/browser/ui/cocoa/tab_window_controller.mm351
-rw-r--r--chrome/browser/ui/cocoa/table_model_array_controller.h54
-rw-r--r--chrome/browser/ui/cocoa/table_model_array_controller.mm246
-rw-r--r--chrome/browser/ui/cocoa/table_model_array_controller_unittest.mm172
-rw-r--r--chrome/browser/ui/cocoa/table_row_nsimage_cache.h55
-rw-r--r--chrome/browser/ui/cocoa/table_row_nsimage_cache.mm79
-rw-r--r--chrome/browser/ui/cocoa/table_row_nsimage_cache_unittest.mm200
-rw-r--r--chrome/browser/ui/cocoa/tabpose_window.h94
-rw-r--r--chrome/browser/ui/cocoa/tabpose_window.mm1437
-rw-r--r--chrome/browser/ui/cocoa/tabpose_window_unittest.mm119
-rw-r--r--chrome/browser/ui/cocoa/task_helpers.h29
-rw-r--r--chrome/browser/ui/cocoa/task_helpers.mm57
-rw-r--r--chrome/browser/ui/cocoa/task_manager_mac.h118
-rw-r--r--chrome/browser/ui/cocoa/task_manager_mac.mm582
-rw-r--r--chrome/browser/ui/cocoa/task_manager_mac_unittest.mm115
-rw-r--r--chrome/browser/ui/cocoa/test_event_utils.h48
-rw-r--r--chrome/browser/ui/cocoa/test_event_utils.mm86
-rw-r--r--chrome/browser/ui/cocoa/theme_install_bubble_view.h57
-rw-r--r--chrome/browser/ui/cocoa/theme_install_bubble_view.mm186
-rw-r--r--chrome/browser/ui/cocoa/themed_window.h30
-rw-r--r--chrome/browser/ui/cocoa/themed_window.mm23
-rw-r--r--chrome/browser/ui/cocoa/throbber_view.h42
-rw-r--r--chrome/browser/ui/cocoa/throbber_view.mm372
-rw-r--r--chrome/browser/ui/cocoa/throbber_view_unittest.mm32
-rw-r--r--chrome/browser/ui/cocoa/toolbar_controller.h189
-rw-r--r--chrome/browser/ui/cocoa/toolbar_controller.mm753
-rw-r--r--chrome/browser/ui/cocoa/toolbar_controller_unittest.mm237
-rw-r--r--chrome/browser/ui/cocoa/toolbar_view.h26
-rw-r--r--chrome/browser/ui/cocoa/toolbar_view.mm47
-rw-r--r--chrome/browser/ui/cocoa/toolbar_view_unittest.mm23
-rw-r--r--chrome/browser/ui/cocoa/translate/after_translate_infobar_controller.h11
-rw-r--r--chrome/browser/ui/cocoa/translate/after_translate_infobar_controller.mm60
-rw-r--r--chrome/browser/ui/cocoa/translate/before_translate_infobar_controller.h23
-rw-r--r--chrome/browser/ui/cocoa/translate/before_translate_infobar_controller.mm123
-rw-r--r--chrome/browser/ui/cocoa/translate/translate_infobar_base.h163
-rw-r--r--chrome/browser/ui/cocoa/translate/translate_infobar_base.mm642
-rw-r--r--chrome/browser/ui/cocoa/translate/translate_infobar_unittest.mm254
-rw-r--r--chrome/browser/ui/cocoa/translate/translate_message_infobar_controller.h10
-rw-r--r--chrome/browser/ui/cocoa/translate/translate_message_infobar_controller.mm53
-rw-r--r--chrome/browser/ui/cocoa/ui_localizer.h35
-rw-r--r--chrome/browser/ui/cocoa/ui_localizer.mm98
-rw-r--r--chrome/browser/ui/cocoa/url_drop_target.h71
-rw-r--r--chrome/browser/ui/cocoa/url_drop_target.mm98
-rw-r--r--chrome/browser/ui/cocoa/vertical_gradient_view.h36
-rw-r--r--chrome/browser/ui/cocoa/vertical_gradient_view.mm39
-rw-r--r--chrome/browser/ui/cocoa/vertical_gradient_view_unittest.mm27
-rw-r--r--chrome/browser/ui/cocoa/view_id_util.h52
-rw-r--r--chrome/browser/ui/cocoa/view_id_util.mm87
-rw-r--r--chrome/browser/ui/cocoa/view_id_util_browsertest.mm118
-rw-r--r--chrome/browser/ui/cocoa/view_resizer.h28
-rw-r--r--chrome/browser/ui/cocoa/view_resizer_pong.h22
-rw-r--r--chrome/browser/ui/cocoa/view_resizer_pong.mm20
-rw-r--r--chrome/browser/ui/cocoa/web_contents_drag_source.h62
-rw-r--r--chrome/browser/ui/cocoa/web_contents_drag_source.mm130
-rw-r--r--chrome/browser/ui/cocoa/web_drag_source.h80
-rw-r--r--chrome/browser/ui/cocoa/web_drag_source.mm412
-rw-r--r--chrome/browser/ui/cocoa/web_drop_target.h80
-rw-r--r--chrome/browser/ui/cocoa/web_drop_target.mm283
-rw-r--r--chrome/browser/ui/cocoa/web_drop_target_unittest.mm166
-rw-r--r--chrome/browser/ui/cocoa/window_size_autosaver.h35
-rw-r--r--chrome/browser/ui/cocoa/window_size_autosaver.mm108
-rw-r--r--chrome/browser/ui/cocoa/window_size_autosaver_unittest.mm201
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu_button_cell.h19
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu_button_cell.mm48
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu_button_cell_unittest.mm51
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu_controller.h72
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu_controller.mm213
-rw-r--r--chrome/browser/ui/cocoa/wrench_menu_controller_unittest.mm84
636 files changed, 95724 insertions, 2 deletions
diff --git a/chrome/browser/ui/browser.cc b/chrome/browser/ui/browser.cc
index 9085f5f..3653aa9 100644
--- a/chrome/browser/ui/browser.cc
+++ b/chrome/browser/ui/browser.cc
@@ -127,7 +127,7 @@
#endif // OS_WIN
#if defined(OS_MACOSX)
-#include "chrome/browser/cocoa/find_pasteboard.h"
+#include "chrome/browser/ui/cocoa/find_pasteboard.h"
#endif
#if defined(OS_CHROMEOS)
diff --git a/chrome/browser/ui/browser_init.cc b/chrome/browser/ui/browser_init.cc
index 0ac4f04..2bd55dc 100644
--- a/chrome/browser/ui/browser_init.cc
+++ b/chrome/browser/ui/browser_init.cc
@@ -69,7 +69,7 @@
#include "webkit/glue/webkit_glue.h"
#if defined(OS_MACOSX)
-#include "chrome/browser/cocoa/keystone_infobar.h"
+#include "chrome/browser/ui/cocoa/keystone_infobar.h"
#endif
#if defined(OS_WIN)
diff --git a/chrome/browser/ui/cocoa/DEPS b/chrome/browser/ui/cocoa/DEPS
new file mode 100644
index 0000000..c00f313
--- /dev/null
+++ b/chrome/browser/ui/cocoa/DEPS
@@ -0,0 +1,4 @@
+include_rules = [
+ "+third_party/molokocacao", # For NSBezierPath additions.
+ "+third_party/ocmock", # For unit tests.
+]
diff --git a/chrome/browser/ui/cocoa/about_ipc_bridge.h b/chrome/browser/ui/cocoa/about_ipc_bridge.h
new file mode 100644
index 0000000..77041b3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/about_ipc_bridge.h
@@ -0,0 +1,33 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_ABOUT_IPC_BRIDGE_H_
+#define CHROME_BROWSER_UI_COCOA_ABOUT_IPC_BRIDGE_H_
+#pragma once
+
+#include "ipc/ipc_logging.h"
+#include "ipc/ipc_message_utils.h"
+
+#if defined(IPC_MESSAGE_LOG_ENABLED)
+
+@class AboutIPCController;
+
+// On Windows, the AboutIPCDialog is a views::View. On Mac we have a
+// Cocoa dialog. This class bridges from C++ to ObjC.
+class AboutIPCBridge : public IPC::Logging::Consumer {
+ public:
+ AboutIPCBridge(AboutIPCController* controller) : controller_(controller) { }
+ virtual ~AboutIPCBridge() { }
+
+ // IPC::Logging::Consumer implementation.
+ virtual void Log(const IPC::LogData& data);
+
+ private:
+ AboutIPCController* controller_; // weak; owns me
+ DISALLOW_COPY_AND_ASSIGN(AboutIPCBridge);
+};
+
+#endif // IPC_MESSAGE_LOG_ENABLED
+
+#endif // CHROME_BROWSER_UI_COCOA_ABOUT_IPC_BRIDGE_H_
diff --git a/chrome/browser/ui/cocoa/about_ipc_bridge.mm b/chrome/browser/ui/cocoa/about_ipc_bridge.mm
new file mode 100644
index 0000000..7a7f41f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/about_ipc_bridge.mm
@@ -0,0 +1,21 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/about_ipc_bridge.h"
+#include "chrome/browser/ui/cocoa/about_ipc_controller.h"
+
+#if defined(IPC_MESSAGE_LOG_ENABLED)
+
+void AboutIPCBridge::Log(const IPC::LogData& data) {
+ CocoaLogData* cocoa_data = [[CocoaLogData alloc] initWithLogData:data];
+ if ([NSThread isMainThread]) {
+ [controller_ log:cocoa_data];
+ } else {
+ [controller_ performSelectorOnMainThread:@selector(log:)
+ withObject:cocoa_data
+ waitUntilDone:NO];
+ }
+}
+
+#endif
diff --git a/chrome/browser/ui/cocoa/about_ipc_controller.h b/chrome/browser/ui/cocoa/about_ipc_controller.h
new file mode 100644
index 0000000..f0818f9
--- /dev/null
+++ b/chrome/browser/ui/cocoa/about_ipc_controller.h
@@ -0,0 +1,84 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_ABOUT_IPC_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_ABOUT_IPC_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_ptr.h"
+#include "ipc/ipc_logging.h"
+#include "ipc/ipc_message_utils.h"
+#include "third_party/GTM/Foundation/GTMRegex.h"
+
+// Must be included after IPC_MESSAGE_LOG_ENABLED gets defined
+#import "chrome/browser/ui/cocoa/about_ipc_bridge.h"
+
+#if defined(IPC_MESSAGE_LOG_ENABLED)
+
+// An objc wrapper for IPC::LogData to allow use of Cocoa bindings.
+@interface CocoaLogData : NSObject {
+ @private
+ IPC::LogData data_;
+}
+- (id)initWithLogData:(const IPC::LogData&)data;
+@end
+
+
+// A window controller that handles the about:ipc non-modal dialog.
+@interface AboutIPCController : NSWindowController {
+ @private
+ scoped_ptr<AboutIPCBridge> bridge_;
+ IBOutlet NSButton* startStopButton_;
+ IBOutlet NSTableView* tableView_;
+ IBOutlet NSArrayController* dataController_;
+ IBOutlet NSTextField* eventCount_;
+ IBOutlet NSTextField* filteredEventCount_;
+ IBOutlet NSTextField* userStringTextField1_;
+ IBOutlet NSTextField* userStringTextField2_;
+ IBOutlet NSTextField* userStringTextField3_;
+ // Count of filtered events.
+ int filteredEventCounter_;
+ // Cocoa-bound to check boxes for filtering messages.
+ // Each BOOL allows events that have that name prefix.
+ // E.g. if set, appCache_ allows events named AppCache*.
+ // The actual string to match is defined in the xib.
+ // The userStrings allow a user-specified prefix.
+ BOOL appCache_;
+ BOOL view_;
+ BOOL utilityHost_;
+ BOOL viewHost_;
+ BOOL plugin_;
+ BOOL npObject_;
+ BOOL devTools_;
+ BOOL pluginProcessing_;
+ BOOL userString1_;
+ BOOL userString2_;
+ BOOL userString3_;
+}
+
++ (AboutIPCController*)sharedController;
+
+- (IBAction)startStop:(id)sender;
+- (IBAction)clear:(id)sender;
+
+// Called from our C++ bridge class. To accomodate multithreaded
+// ownership issues, this method ACCEPTS OWNERSHIP of the arg passed
+// in.
+- (void)log:(CocoaLogData*)data;
+
+// Update visible state (e.g. Start/Stop button) based on logging run
+// state. Does not change state.
+- (void)updateVisibleRunState;
+
+@end
+
+@interface AboutIPCController(TestingAPI)
+- (BOOL)filterOut:(CocoaLogData*)data;
+- (void)setDisplayViewMessages:(BOOL)display;
+@end
+
+#endif // IPC_MESSAGE_LOG_ENABLED
+#endif // CHROME_BROWSER_UI_COCOA_ABOUT_IPC_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/about_ipc_controller.mm b/chrome/browser/ui/cocoa/about_ipc_controller.mm
new file mode 100644
index 0000000..df6ae24
--- /dev/null
+++ b/chrome/browser/ui/cocoa/about_ipc_controller.mm
@@ -0,0 +1,198 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/logging.h"
+#include "base/mac_util.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "base/time.h"
+#include "chrome/browser/browser_process.h"
+#import "chrome/browser/ui/cocoa/about_ipc_controller.h"
+
+#if defined(IPC_MESSAGE_LOG_ENABLED)
+
+@implementation CocoaLogData
+
+- (id)initWithLogData:(const IPC::LogData&)data {
+ if ((self = [super init])) {
+ data_ = data;
+ // data_.message_name may not have been filled in if it originated
+ // somewhere other than the browser process.
+ IPC::Logging::GetMessageText(data_.type, &data_.message_name, NULL, NULL);
+ }
+ return self;
+}
+
+- (NSString*)time {
+ base::Time t = base::Time::FromInternalValue(data_.sent);
+ base::Time::Exploded exploded;
+ t.LocalExplode(&exploded);
+ return [NSString stringWithFormat:@"%02d:%02d:%02d.%03d",
+ exploded.hour, exploded.minute,
+ exploded.second, exploded.millisecond];
+}
+
+- (NSString*)channel {
+ return base::SysUTF8ToNSString(data_.channel);
+}
+
+- (NSString*)message {
+ if (data_.message_name == "") {
+ int high = data_.type >> 12;
+ int low = data_.type - (high<<12);
+ return [NSString stringWithFormat:@"type=(%d,%d) 0x%x,0x%x",
+ high, low, high, low];
+ }
+ else {
+ return base::SysUTF8ToNSString(data_.message_name);
+ }
+}
+
+- (NSString*)flags {
+ return base::SysUTF8ToNSString(data_.flags);
+}
+
+- (NSString*)dispatch {
+ base::Time sent = base::Time::FromInternalValue(data_.sent);
+ int64 delta = (base::Time::FromInternalValue(data_.receive) -
+ sent).InMilliseconds();
+ return [NSString stringWithFormat:@"%d", delta ? (int)delta : 0];
+}
+
+- (NSString*)process {
+ base::TimeDelta delta = (base::Time::FromInternalValue(data_.dispatch) -
+ base::Time::FromInternalValue(data_.receive));
+ int64 t = delta.InMilliseconds();
+ return [NSString stringWithFormat:@"%d", t ? (int)t : 0];
+}
+
+- (NSString*)parameters {
+ return base::SysUTF8ToNSString(data_.params);
+}
+
+@end
+
+namespace {
+AboutIPCController* gSharedController = nil;
+}
+
+@implementation AboutIPCController
+
++ (AboutIPCController*)sharedController {
+ if (gSharedController == nil)
+ gSharedController = [[AboutIPCController alloc] init];
+ return gSharedController;
+}
+
+- (id)init {
+ NSString* nibpath = [mac_util::MainAppBundle() pathForResource:@"AboutIPC"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ // Default to all on
+ appCache_ = view_ = utilityHost_ = viewHost_ = plugin_ =
+ npObject_ = devTools_ = pluginProcessing_ = userString1_ =
+ userString2_ = userString3_ = YES;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ if (gSharedController == self)
+ gSharedController = nil;
+ if (g_browser_process)
+ g_browser_process->SetIPCLoggingEnabled(false); // just in case...
+ IPC::Logging::current()->SetConsumer(NULL);
+ [super dealloc];
+}
+
+- (void)awakeFromNib {
+ // Running Chrome with the --ipc-logging switch might cause it to
+ // be enabled before the about:ipc window comes up; accomodate.
+ [self updateVisibleRunState];
+
+ // We are now able to display information, so let'er rip.
+ bridge_.reset(new AboutIPCBridge(self));
+ IPC::Logging::current()->SetConsumer(bridge_.get());
+}
+
+// Delegate callback. Closing the window means there is no more need
+// for the me, the controller.
+- (void)windowWillClose:(NSNotification*)notification {
+ [self autorelease];
+}
+
+- (void)updateVisibleRunState {
+ if (IPC::Logging::current()->Enabled())
+ [startStopButton_ setTitle:@"Stop"];
+ else
+ [startStopButton_ setTitle:@"Start"];
+}
+
+- (IBAction)startStop:(id)sender {
+ g_browser_process->SetIPCLoggingEnabled(!IPC::Logging::current()->Enabled());
+ [self updateVisibleRunState];
+}
+
+- (IBAction)clear:(id)sender {
+ [dataController_ setContent:[NSMutableArray array]];
+ [eventCount_ setStringValue:@"0"];
+ [filteredEventCount_ setStringValue:@"0"];
+ filteredEventCounter_ = 0;
+}
+
+// Return YES if we should filter this out; else NO.
+// Just to be clear, [@"any string" hasPrefix:@""] returns NO.
+- (BOOL)filterOut:(CocoaLogData*)data {
+ NSString* name = [data message];
+ if ((appCache_) && [name hasPrefix:@"AppCache"])
+ return NO;
+ if ((view_) && [name hasPrefix:@"ViewMsg"])
+ return NO;
+ if ((utilityHost_) && [name hasPrefix:@"UtilityHost"])
+ return NO;
+ if ((viewHost_) && [name hasPrefix:@"ViewHost"])
+ return NO;
+ if ((plugin_) && [name hasPrefix:@"PluginMsg"])
+ return NO;
+ if ((npObject_) && [name hasPrefix:@"NPObject"])
+ return NO;
+ if ((devTools_) && [name hasPrefix:@"DevTools"])
+ return NO;
+ if ((pluginProcessing_) && [name hasPrefix:@"PluginProcessing"])
+ return NO;
+ if ((userString1_) && ([name hasPrefix:[userStringTextField1_ stringValue]]))
+ return NO;
+ if ((userString2_) && ([name hasPrefix:[userStringTextField2_ stringValue]]))
+ return NO;
+ if ((userString3_) && ([name hasPrefix:[userStringTextField3_ stringValue]]))
+ return NO;
+
+ // Special case the unknown type.
+ if ([name hasPrefix:@"type="])
+ return NO;
+
+ return YES; // filter out.
+}
+
+- (void)log:(CocoaLogData*)data {
+ if ([self filterOut:data]) {
+ [filteredEventCount_ setStringValue:[NSString stringWithFormat:@"%d",
+ ++filteredEventCounter_]];
+ return;
+ }
+ [dataController_ addObject:data];
+ NSUInteger count = [[dataController_ arrangedObjects] count];
+ // Uncomment if you want scroll-to-end behavior... but seems expensive.
+ // [tableView_ scrollRowToVisible:count-1];
+ [eventCount_ setStringValue:[NSString stringWithFormat:@"%d", count]];
+}
+
+- (void)setDisplayViewMessages:(BOOL)display {
+ view_ = display;
+}
+
+@end
+
+#endif // IPC_MESSAGE_LOG_ENABLED
+
diff --git a/chrome/browser/ui/cocoa/about_ipc_controller_unittest.mm b/chrome/browser/ui/cocoa/about_ipc_controller_unittest.mm
new file mode 100644
index 0000000..afde86e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/about_ipc_controller_unittest.mm
@@ -0,0 +1,50 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/about_ipc_controller.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+#if defined(IPC_MESSAGE_LOG_ENABLED)
+
+namespace {
+
+class AboutIPCControllerTest : public CocoaTest {
+};
+
+TEST_F(AboutIPCControllerTest, TestFilter) {
+ AboutIPCController* controller = [[AboutIPCController alloc] init];
+ EXPECT_TRUE([controller window]); // force nib load.
+ IPC::LogData data;
+
+ // Make sure generic names do NOT get filtered.
+ std::string names[] = { "PluginProcessingIsMyLife",
+ "ViewMsgFoo",
+ "NPObjectHell" };
+ for (size_t i = 0; i < arraysize(names); i++) {
+ data.message_name = names[i];
+ scoped_nsobject<CocoaLogData> cdata([[CocoaLogData alloc]
+ initWithLogData:data]);
+ EXPECT_FALSE([controller filterOut:cdata.get()]);
+ }
+
+ // Flip a checkbox, see it filtered, flip back, all is fine.
+ data.message_name = "ViewMsgFoo";
+ scoped_nsobject<CocoaLogData> cdata([[CocoaLogData alloc]
+ initWithLogData:data]);
+ [controller setDisplayViewMessages:NO];
+ EXPECT_TRUE([controller filterOut:cdata.get()]);
+ [controller setDisplayViewMessages:YES];
+ EXPECT_FALSE([controller filterOut:cdata.get()]);
+ [controller close];
+}
+
+} // namespace
+
+#endif // IPC_MESSAGE_LOG_ENABLED
diff --git a/chrome/browser/ui/cocoa/about_ipc_dialog.h b/chrome/browser/ui/cocoa/about_ipc_dialog.h
new file mode 100644
index 0000000..3eb2bcd
--- /dev/null
+++ b/chrome/browser/ui/cocoa/about_ipc_dialog.h
@@ -0,0 +1,24 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_ABOUT_IPC_DIALOG_H_
+#define CHROME_BROWSER_UI_COCOA_ABOUT_IPC_DIALOG_H_
+#pragma once
+
+#include "ipc/ipc_message.h"
+
+#if defined(IPC_MESSAGE_LOG_ENABLED)
+
+namespace AboutIPCDialog {
+// The dialog is a singleton. If the dialog is already opened, it won't do
+// anything, so you can just blindly call this function all you want.
+// RunDialog() is Called from chrome/browser/browser_about_handler.cc
+// in response to an about:ipc URL.
+void RunDialog();
+};
+
+
+#endif /* IPC_MESSAGE_LOG_ENABLED */
+
+#endif /* CHROME_BROWSER_UI_COCOA_ABOUT_IPC_DIALOG_H_ */
diff --git a/chrome/browser/ui/cocoa/about_ipc_dialog.mm b/chrome/browser/ui/cocoa/about_ipc_dialog.mm
new file mode 100644
index 0000000..7715f24
--- /dev/null
+++ b/chrome/browser/ui/cocoa/about_ipc_dialog.mm
@@ -0,0 +1,21 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/about_ipc_dialog.h"
+#include "chrome/browser/ui/cocoa/about_ipc_controller.h"
+
+#if defined(IPC_MESSAGE_LOG_ENABLED)
+
+namespace AboutIPCDialog {
+
+void RunDialog() {
+ // The controller gets deallocated when then window is closed,
+ // so it is safe to "fire and forget".
+ AboutIPCController* controller = [AboutIPCController sharedController];
+ [[controller window] makeKeyAndOrderFront:controller];
+}
+
+}; // namespace AboutIPCDialog
+
+#endif // IPC_MESSAGE_LOG_ENABLED
diff --git a/chrome/browser/ui/cocoa/about_window_controller.h b/chrome/browser/ui/cocoa/about_window_controller.h
new file mode 100644
index 0000000..f5b9851
--- /dev/null
+++ b/chrome/browser/ui/cocoa/about_window_controller.h
@@ -0,0 +1,69 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_ABOUT_WINDOW_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_ABOUT_WINDOW_CONTROLLER_H_
+#pragma once
+
+#import <AppKit/AppKit.h>
+
+@class BackgroundTileView;
+class Profile;
+
+// This simple subclass of |NSTextView| just doesn't show the (text) cursor
+// (|NSTextView| displays the cursor with full keyboard accessibility enabled).
+@interface AboutLegalTextView : NSTextView
+@end
+
+// A window controller that handles the About box.
+@interface AboutWindowController : NSWindowController {
+ @private
+ IBOutlet NSTextField* version_;
+ IBOutlet BackgroundTileView* backgroundView_;
+ IBOutlet NSImageView* logoView_;
+ IBOutlet NSView* legalBlock_;
+ IBOutlet AboutLegalTextView* legalText_;
+
+ // updateBlock_ holds the update image or throbber, update text, and update
+ // button.
+ IBOutlet NSView* updateBlock_;
+
+ IBOutlet NSProgressIndicator* spinner_;
+ IBOutlet NSImageView* updateStatusIndicator_;
+ IBOutlet NSTextField* updateText_;
+ IBOutlet NSButton* updateNowButton_;
+ IBOutlet NSButton* promoteButton_;
+
+ Profile* profile_; // Weak, probably the default profile.
+
+ // The window frame height. During an animation, this will contain the
+ // height being animated to.
+ CGFloat windowHeight_;
+}
+
+// Initialize the controller with the given profile, but does not show it.
+// Callers still need to call showWindow: to put it on screen.
+- (id)initWithProfile:(Profile*)profile;
+
+// Trigger an update right now, as initiated by a button.
+- (IBAction)updateNow:(id)sender;
+
+// Install a system Keystone if necessary and promote the ticket to a system
+// ticket.
+- (IBAction)promoteUpdater:(id)sender;
+
+@end // @interface AboutWindowController
+
+@interface AboutWindowController(JustForTesting)
+
+- (NSTextView*)legalText;
+- (NSButton*)updateButton;
+- (NSTextField*)updateText;
+
+// Returns an NSAttributedString that contains locale-specific legal text.
++ (NSAttributedString*)legalTextBlock;
+
+@end // @interface AboutWindowController(JustForTesting)
+
+#endif // CHROME_BROWSER_UI_COCOA_ABOUT_WINDOW_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/about_window_controller.mm b/chrome/browser/ui/cocoa/about_window_controller.mm
new file mode 100644
index 0000000..f6da6ab
--- /dev/null
+++ b/chrome/browser/ui/cocoa/about_window_controller.mm
@@ -0,0 +1,761 @@
+// 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/ui/cocoa/about_window_controller.h"
+
+#include "app/l10n_util.h"
+#include "app/l10n_util_mac.h"
+#include "app/resource_bundle.h"
+#include "base/logging.h"
+#include "base/mac_util.h"
+#include "base/string_number_conversions.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/browser_list.h"
+#include "chrome/browser/browser_window.h"
+#include "chrome/browser/platform_util.h"
+#import "chrome/browser/ui/cocoa/background_tile_view.h"
+#import "chrome/browser/ui/cocoa/keystone_glue.h"
+#include "chrome/browser/ui/cocoa/restart_browser.h"
+#include "chrome/common/url_constants.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#include "grit/locale_settings.h"
+#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+namespace {
+
+void AttributedStringAppendString(NSMutableAttributedString* attr_str,
+ NSString* str) {
+ // You might think doing [[attr_str mutableString] appendString:str] would
+ // work, but it causes any trailing style to get extened, meaning as we
+ // append links, they grow to include the new text, not what we want.
+ NSAttributedString* new_attr_str =
+ [[[NSAttributedString alloc] initWithString:str] autorelease];
+ [attr_str appendAttributedString:new_attr_str];
+}
+
+void AttributedStringAppendHyperlink(NSMutableAttributedString* attr_str,
+ NSString* text, NSString* url_str) {
+ // Figure out the range of the text we're adding and add the text.
+ NSRange range = NSMakeRange([attr_str length], [text length]);
+ AttributedStringAppendString(attr_str, text);
+
+ // Add the link
+ [attr_str addAttribute:NSLinkAttributeName value:url_str range:range];
+
+ // Blue and underlined
+ [attr_str addAttribute:NSForegroundColorAttributeName
+ value:[NSColor blueColor]
+ range:range];
+ [attr_str addAttribute:NSUnderlineStyleAttributeName
+ value:[NSNumber numberWithInt:NSSingleUnderlineStyle]
+ range:range];
+ [attr_str addAttribute:NSCursorAttributeName
+ value:[NSCursor pointingHandCursor]
+ range:range];
+}
+
+} // namespace
+
+@interface AboutWindowController(Private)
+
+// Launches a check for available updates.
+- (void)checkForUpdate;
+
+// Turns the update and promotion blocks on and off as needed based on whether
+// updates are possible and promotion is desired or required.
+- (void)adjustUpdateUIVisibility;
+
+// Maintains the update and promotion block visibility and window sizing.
+// This uses bool instead of BOOL for the convenience of the internal
+// implementation.
+- (void)setAllowsUpdate:(bool)update allowsPromotion:(bool)promotion;
+
+// Notification callback, called with the status of asynchronous
+// -checkForUpdate and -updateNow: operations.
+- (void)updateStatus:(NSNotification*)notification;
+
+// These methods maintain the image (or throbber) and text displayed regarding
+// update status. -setUpdateThrobberMessage: starts a progress throbber and
+// sets the text. -setUpdateImage:message: displays an image and sets the
+// text.
+- (void)setUpdateThrobberMessage:(NSString*)message;
+- (void)setUpdateImage:(int)imageID message:(NSString*)message;
+
+@end // @interface AboutWindowController(Private)
+
+@implementation AboutLegalTextView
+
+// Never draw the insertion point (otherwise, it shows up without any user
+// action if full keyboard accessibility is enabled).
+- (BOOL)shouldDrawInsertionPoint {
+ return NO;
+}
+
+@end
+
+@implementation AboutWindowController
+
+- (id)initWithProfile:(Profile*)profile {
+ NSString* nibPath = [mac_util::MainAppBundle() pathForResource:@"About"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
+ profile_ = profile;
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
+ selector:@selector(updateStatus:)
+ name:kAutoupdateStatusNotification
+ object:nil];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+// YES when an About box is currently showing the kAutoupdateInstallFailed
+// status, or if no About box is visible, if the most recent About box to be
+// closed was closed while showing this status. When an About box opens, if
+// the recent status is kAutoupdateInstallFailed or kAutoupdatePromoteFailed
+// and recentShownUserActionFailedStatus is NO, the failure needs to be shown
+// instead of launching a new update check. recentShownInstallFailedStatus is
+// maintained by -updateStatus:.
+static BOOL recentShownUserActionFailedStatus = NO;
+
+- (void)awakeFromNib {
+ NSBundle* bundle = mac_util::MainAppBundle();
+ NSString* chromeVersion =
+ [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
+
+ NSString* versionModifier = @"";
+ NSString* svnRevision = @"";
+ std::string modifier = platform_util::GetVersionStringModifier();
+ if (!modifier.empty())
+ versionModifier = [NSString stringWithFormat:@" %@",
+ base::SysUTF8ToNSString(modifier)];
+
+#if !defined(GOOGLE_CHROME_BUILD)
+ svnRevision = [NSString stringWithFormat:@" (%@)",
+ [bundle objectForInfoDictionaryKey:@"SVNRevision"]];
+#endif
+ // The format string is not localized, but this is how the displayed version
+ // is built on Windows too.
+ NSString* version =
+ [NSString stringWithFormat:@"%@%@%@",
+ chromeVersion, svnRevision, versionModifier];
+
+ [version_ setStringValue:version];
+
+ // Put the two images into the UI.
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ NSImage* backgroundImage = rb.GetNativeImageNamed(IDR_ABOUT_BACKGROUND_COLOR);
+ DCHECK(backgroundImage);
+ [backgroundView_ setTileImage:backgroundImage];
+ NSImage* logoImage = rb.GetNativeImageNamed(IDR_ABOUT_BACKGROUND);
+ DCHECK(logoImage);
+ [logoView_ setImage:logoImage];
+
+ [[legalText_ textStorage] setAttributedString:[[self class] legalTextBlock]];
+
+ // Resize our text view now so that the |updateShift| below is set
+ // correctly. The About box has its controls manually positioned, so we need
+ // to calculate how much larger (or smaller) our text box is and store that
+ // difference in |legalShift|. We do something similar with |updateShift|
+ // below, which is either 0, or the amount of space to offset the window size
+ // because the view that contains the update button has been removed because
+ // this build doesn't have Keystone.
+ NSRect oldLegalRect = [legalBlock_ frame];
+ [legalText_ sizeToFit];
+ NSRect newRect = oldLegalRect;
+ newRect.size.height = [legalText_ frame].size.height;
+ [legalBlock_ setFrame:newRect];
+ CGFloat legalShift = newRect.size.height - oldLegalRect.size.height;
+
+ NSRect backgroundFrame = [backgroundView_ frame];
+ backgroundFrame.origin.y += legalShift;
+ [backgroundView_ setFrame:backgroundFrame];
+
+ NSSize windowDelta = NSMakeSize(0.0, legalShift);
+ [GTMUILocalizerAndLayoutTweaker
+ resizeWindowWithoutAutoResizingSubViews:[self window]
+ delta:windowDelta];
+
+ windowHeight_ = [[self window] frame].size.height;
+
+ [self adjustUpdateUIVisibility];
+
+ // Don't do anything update-related if adjustUpdateUIVisibility decided that
+ // updates aren't possible.
+ if (![updateBlock_ isHidden]) {
+ KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue];
+ AutoupdateStatus recentStatus = [keystoneGlue recentStatus];
+ if ([keystoneGlue asyncOperationPending] ||
+ recentStatus == kAutoupdateRegisterFailed ||
+ ((recentStatus == kAutoupdateInstallFailed ||
+ recentStatus == kAutoupdatePromoteFailed) &&
+ !recentShownUserActionFailedStatus)) {
+ // If an asynchronous update operation is currently pending, such as a
+ // check for updates or an update installation attempt, set the status
+ // up correspondingly without launching a new update check.
+ //
+ // If registration failed, no other operations make sense, so just go
+ // straight to the error.
+ //
+ // If a previous update or promotion attempt was unsuccessful but no
+ // About box was around to report the error, show it now, and allow
+ // another chance to perform the action.
+ [self updateStatus:[keystoneGlue recentNotification]];
+ } else {
+ // Launch a new update check, even if one was already completed, because
+ // a new update may be available or a new update may have been installed
+ // in the background since the last time an About box was displayed.
+ [self checkForUpdate];
+ }
+ }
+
+ [[self window] center];
+}
+
+- (void)windowWillClose:(NSNotification*)notification {
+ [self autorelease];
+}
+
+- (void)adjustUpdateUIVisibility {
+ bool allowUpdate;
+ bool allowPromotion;
+
+ KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue];
+ if (keystoneGlue && ![keystoneGlue isOnReadOnlyFilesystem]) {
+ AutoupdateStatus recentStatus = [keystoneGlue recentStatus];
+ if (recentStatus == kAutoupdateRegistering ||
+ recentStatus == kAutoupdateRegisterFailed ||
+ recentStatus == kAutoupdatePromoted) {
+ // Show the update block while registering so that there's a progress
+ // spinner, and if registration failed so that there's an error message.
+ // Show it following a promotion because updates should be possible
+ // after promotion successfully completes.
+ allowUpdate = true;
+
+ // Promotion isn't possible at this point.
+ allowPromotion = false;
+ } else if (recentStatus == kAutoupdatePromoteFailed) {
+ // TODO(mark): Add kAutoupdatePromoting to this block. KSRegistration
+ // currently handles the promotion synchronously, meaning that the main
+ // thread's loop doesn't spin, meaning that animations and other updates
+ // to the window won't occur until KSRegistration is done with
+ // promotion. This looks laggy and bad and probably qualifies as
+ // "jank." For now, there just won't be any visual feedback while
+ // promotion is in progress, but it should complete (or fail) very
+ // quickly. http://b/2290009.
+ //
+ // Also see the TODO for kAutoupdatePromoting in -updateStatus:version:.
+ //
+ // Show the update block so that there's some visual feedback that
+ // promotion is under way or that it's failed. Show the promotion block
+ // because the user either just clicked that button or because the user
+ // should be able to click it again.
+ allowUpdate = true;
+ allowPromotion = true;
+ } else {
+ // Show the update block only if a promotion is not absolutely required.
+ allowUpdate = ![keystoneGlue needsPromotion];
+
+ // Show the promotion block if promotion is a possibility.
+ allowPromotion = [keystoneGlue wantsPromotion];
+ }
+ } else {
+ // There is no glue, or the application is on a read-only filesystem.
+ // Updates and promotions are impossible.
+ allowUpdate = false;
+ allowPromotion = false;
+ }
+
+ [self setAllowsUpdate:allowUpdate allowsPromotion:allowPromotion];
+}
+
+- (void)setAllowsUpdate:(bool)update allowsPromotion:(bool)promotion {
+ bool oldUpdate = ![updateBlock_ isHidden];
+ bool oldPromotion = ![promoteButton_ isHidden];
+
+ if (promotion == oldPromotion && update == oldUpdate) {
+ return;
+ }
+
+ NSRect updateFrame = [updateBlock_ frame];
+ CGFloat delta = 0.0;
+
+ if (update != oldUpdate) {
+ [updateBlock_ setHidden:!update];
+ delta += (update ? 1.0 : -1.0) * NSHeight(updateFrame);
+ }
+
+ if (promotion != oldPromotion) {
+ [promoteButton_ setHidden:!promotion];
+ }
+
+ NSRect legalFrame = [legalBlock_ frame];
+
+ if (delta) {
+ updateFrame.origin.y += delta;
+ [updateBlock_ setFrame:updateFrame];
+
+ legalFrame.origin.y += delta;
+ [legalBlock_ setFrame:legalFrame];
+
+ NSRect backgroundFrame = [backgroundView_ frame];
+ backgroundFrame.origin.y += delta;
+ [backgroundView_ setFrame:backgroundFrame];
+
+ // GTMUILocalizerAndLayoutTweaker resizes the window without any
+ // opportunity for animation. In order to animate, disable window
+ // updates, save the current frame, let GTMUILocalizerAndLayoutTweaker do
+ // its thing, save the desired frame, restore the original frame, and then
+ // animate.
+ NSWindow* window = [self window];
+ [window disableScreenUpdatesUntilFlush];
+
+ NSRect oldFrame = [window frame];
+
+ // GTMUILocalizerAndLayoutTweaker applies its delta to the window's
+ // current size (like oldFrame.size), but oldFrame isn't trustworthy if
+ // an animation is in progress. Set the window's frame to
+ // intermediateFrame, which is a frame of the size that an existing
+ // animation is animating to, so that GTM can apply the delta to the right
+ // size.
+ NSRect intermediateFrame = oldFrame;
+ intermediateFrame.origin.y -= intermediateFrame.size.height - windowHeight_;
+ intermediateFrame.size.height = windowHeight_;
+ [window setFrame:intermediateFrame display:NO];
+
+ NSSize windowDelta = NSMakeSize(0.0, delta);
+ [GTMUILocalizerAndLayoutTweaker
+ resizeWindowWithoutAutoResizingSubViews:window
+ delta:windowDelta];
+ [window setFrameTopLeftPoint:NSMakePoint(NSMinX(intermediateFrame),
+ NSMaxY(intermediateFrame))];
+ NSRect newFrame = [window frame];
+
+ windowHeight_ += delta;
+
+ if (![[self window] isVisible]) {
+ // Don't animate if the window isn't on screen yet.
+ [window setFrame:newFrame display:NO];
+ } else {
+ [window setFrame:oldFrame display:NO];
+ [window setFrame:newFrame display:YES animate:YES];
+ }
+ }
+}
+
+- (void)setUpdateThrobberMessage:(NSString*)message {
+ [updateStatusIndicator_ setHidden:YES];
+
+ [spinner_ setHidden:NO];
+ [spinner_ startAnimation:self];
+
+ [updateText_ setStringValue:message];
+}
+
+- (void)setUpdateImage:(int)imageID message:(NSString*)message {
+ [spinner_ stopAnimation:self];
+ [spinner_ setHidden:YES];
+
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ NSImage* statusImage = rb.GetNativeImageNamed(imageID);
+ DCHECK(statusImage);
+ [updateStatusIndicator_ setImage:statusImage];
+ [updateStatusIndicator_ setHidden:NO];
+
+ [updateText_ setStringValue:message];
+}
+
+- (void)checkForUpdate {
+ [[KeystoneGlue defaultKeystoneGlue] checkForUpdate];
+
+ // Immediately, kAutoupdateStatusNotification will be posted, and
+ // -updateStatus: will be called with status kAutoupdateChecking.
+ //
+ // Upon completion, kAutoupdateStatusNotification will be posted, and
+ // -updateStatus: will be called with a status indicating the result of the
+ // check.
+}
+
+- (IBAction)updateNow:(id)sender {
+ [[KeystoneGlue defaultKeystoneGlue] installUpdate];
+
+ // Immediately, kAutoupdateStatusNotification will be posted, and
+ // -updateStatus: will be called with status kAutoupdateInstalling.
+ //
+ // Upon completion, kAutoupdateStatusNotification will be posted, and
+ // -updateStatus: will be called with a status indicating the result of the
+ // installation attempt.
+}
+
+- (IBAction)promoteUpdater:(id)sender {
+ [[KeystoneGlue defaultKeystoneGlue] promoteTicket];
+
+ // Immediately, kAutoupdateStatusNotification will be posted, and
+ // -updateStatus: will be called with status kAutoupdatePromoting.
+ //
+ // Upon completion, kAutoupdateStatusNotification will be posted, and
+ // -updateStatus: will be called with a status indicating a result of the
+ // installation attempt.
+ //
+ // If the promotion was successful, KeystoneGlue will re-register the ticket
+ // and -updateStatus: will be called again indicating first that
+ // registration is in progress and subsequently that it has completed.
+}
+
+- (void)updateStatus:(NSNotification*)notification {
+ recentShownUserActionFailedStatus = NO;
+
+ NSDictionary* dictionary = [notification userInfo];
+ AutoupdateStatus status = static_cast<AutoupdateStatus>(
+ [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]);
+
+ // Don't assume |version| is a real string. It may be nil.
+ NSString* version = [dictionary objectForKey:kAutoupdateStatusVersion];
+
+ bool updateMessage = true;
+ bool throbber = false;
+ int imageID = 0;
+ NSString* message;
+ bool enableUpdateButton = false;
+ bool enablePromoteButton = true;
+
+ switch (status) {
+ case kAutoupdateRegistering:
+ // When registering, use the "checking" message. The check will be
+ // launched if appropriate immediately after registration.
+ throbber = true;
+ message = l10n_util::GetNSStringWithFixup(IDS_UPGRADE_CHECK_STARTED);
+ enablePromoteButton = false;
+
+ break;
+
+ case kAutoupdateRegistered:
+ // Once registered, the ability to update and promote is known.
+ [self adjustUpdateUIVisibility];
+
+ if (![updateBlock_ isHidden]) {
+ // If registration completes while the window is visible, go straight
+ // into an update check. Return immediately, this routine will be
+ // re-entered shortly with kAutoupdateChecking.
+ [self checkForUpdate];
+ return;
+ }
+
+ // Nothing actually failed, but updates aren't possible. The throbber
+ // and message are hidden, but they'll be reset to these dummy values
+ // just to get the throbber to stop spinning if it's running.
+ imageID = IDR_UPDATE_FAIL;
+ message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR,
+ base::IntToString16(status));
+
+ break;
+
+ case kAutoupdateChecking:
+ throbber = true;
+ message = l10n_util::GetNSStringWithFixup(IDS_UPGRADE_CHECK_STARTED);
+ enablePromoteButton = false;
+
+ break;
+
+ case kAutoupdateCurrent:
+ imageID = IDR_UPDATE_UPTODATE;
+ message = l10n_util::GetNSStringFWithFixup(
+ IDS_UPGRADE_ALREADY_UP_TO_DATE,
+ l10n_util::GetStringUTF16(IDS_PRODUCT_NAME),
+ base::SysNSStringToUTF16(version));
+
+ break;
+
+ case kAutoupdateAvailable:
+ imageID = IDR_UPDATE_AVAILABLE;
+ message = l10n_util::GetNSStringFWithFixup(
+ IDS_UPGRADE_AVAILABLE, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
+ enableUpdateButton = true;
+
+ break;
+
+ case kAutoupdateInstalling:
+ throbber = true;
+ message = l10n_util::GetNSStringWithFixup(IDS_UPGRADE_STARTED);
+ enablePromoteButton = false;
+
+ break;
+
+ case kAutoupdateInstalled:
+ {
+ imageID = IDR_UPDATE_UPTODATE;
+ string16 productName = l10n_util::GetStringUTF16(IDS_PRODUCT_NAME);
+ if (version) {
+ message = l10n_util::GetNSStringFWithFixup(
+ IDS_UPGRADE_SUCCESSFUL,
+ productName,
+ base::SysNSStringToUTF16(version));
+ } else {
+ message = l10n_util::GetNSStringFWithFixup(
+ IDS_UPGRADE_SUCCESSFUL_NOVERSION, productName);
+ }
+
+ // TODO(mark): Turn the button in the dialog into a restart button
+ // instead of springing this sheet or dialog.
+ NSWindow* window = [self window];
+ NSWindow* restartDialogParent = [window isVisible] ? window : nil;
+ restart_browser::RequestRestart(restartDialogParent);
+ }
+
+ break;
+
+ case kAutoupdatePromoting:
+#if 1
+ // TODO(mark): See the TODO in -adjustUpdateUIVisibility for an
+ // explanation of why nothing can be done here at the moment. When
+ // KSRegistration handles promotion asynchronously, this dummy block can
+ // be replaced with the #else block. For now, just leave the messaging
+ // alone. http://b/2290009.
+ updateMessage = false;
+#else
+ // The visibility may be changing.
+ [self adjustUpdateUIVisibility];
+
+ // This is not a terminal state, and kAutoupdatePromoted or
+ // kAutoupdatePromoteFailed will follow. Use the throbber and
+ // "checking" message so that it looks like something's happening.
+ throbber = true;
+ message = l10n_util::GetNSStringWithFixup(IDS_UPGRADE_CHECK_STARTED);
+#endif
+
+ enablePromoteButton = false;
+
+ break;
+
+ case kAutoupdatePromoted:
+ // The visibility may be changing.
+ [self adjustUpdateUIVisibility];
+
+ if (![updateBlock_ isHidden]) {
+ // If promotion completes while the window is visible, go straight
+ // into an update check. Return immediately, this routine will be
+ // re-entered shortly with kAutoupdateChecking.
+ [self checkForUpdate];
+ return;
+ }
+
+ // Nothing actually failed, but updates aren't possible. The throbber
+ // and message are hidden, but they'll be reset to these dummy values
+ // just to get the throbber to stop spinning if it's running.
+ imageID = IDR_UPDATE_FAIL;
+ message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR,
+ base::IntToString16(status));
+
+ break;
+
+ case kAutoupdateRegisterFailed:
+ imageID = IDR_UPDATE_FAIL;
+ message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR,
+ base::IntToString16(status));
+ enablePromoteButton = false;
+
+ break;
+
+ case kAutoupdateCheckFailed:
+ imageID = IDR_UPDATE_FAIL;
+ message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR,
+ base::IntToString16(status));
+
+ break;
+
+ case kAutoupdateInstallFailed:
+ recentShownUserActionFailedStatus = YES;
+
+ imageID = IDR_UPDATE_FAIL;
+ message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR,
+ base::IntToString16(status));
+
+ // Allow another chance.
+ enableUpdateButton = true;
+
+ break;
+
+ case kAutoupdatePromoteFailed:
+ recentShownUserActionFailedStatus = YES;
+
+ imageID = IDR_UPDATE_FAIL;
+ message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR,
+ base::IntToString16(status));
+
+ break;
+
+ default:
+ NOTREACHED();
+
+ return;
+ }
+
+ if (updateMessage) {
+ if (throbber) {
+ [self setUpdateThrobberMessage:message];
+ } else {
+ DCHECK_NE(imageID, 0);
+ [self setUpdateImage:imageID message:message];
+ }
+ }
+
+ // Note that these buttons may be hidden depending on what
+ // -adjustUpdateUIVisibility did. Their enabled/disabled status doesn't
+ // necessarily have anything to do with their visibility.
+ [updateNowButton_ setEnabled:enableUpdateButton];
+ [promoteButton_ setEnabled:enablePromoteButton];
+}
+
+- (BOOL)textView:(NSTextView *)aTextView
+ clickedOnLink:(id)link
+ atIndex:(NSUInteger)charIndex {
+ // We always create a new window, so there's no need to try to re-use
+ // an existing one just to pass in the NEW_WINDOW disposition.
+ Browser* browser = Browser::Create(profile_);
+ browser->OpenURL(GURL([link UTF8String]), GURL(), NEW_FOREGROUND_TAB,
+ PageTransition::LINK);
+ browser->window()->Show();
+ return YES;
+}
+
+- (NSTextView*)legalText {
+ return legalText_;
+}
+
+- (NSButton*)updateButton {
+ return updateNowButton_;
+}
+
+- (NSTextField*)updateText {
+ return updateText_;
+}
+
++ (NSAttributedString*)legalTextBlock {
+ // Windows builds this up in a very complex way, we're just trying to model
+ // it the best we can to get all the information in (they actually do it
+ // but created Labels and Links that they carefully place to make it appear
+ // to be a paragraph of text).
+ // src/chrome/browser/views/about_chrome_view.cc AboutChromeView::Init()
+
+ NSMutableAttributedString* legal_block =
+ [[[NSMutableAttributedString alloc] init] autorelease];
+ [legal_block beginEditing];
+
+ NSString* copyright =
+ l10n_util::GetNSStringWithFixup(IDS_ABOUT_VERSION_COPYRIGHT);
+ AttributedStringAppendString(legal_block, copyright);
+
+ // These are the markers directly in IDS_ABOUT_VERSION_LICENSE
+ NSString* kBeginLinkChr = @"BEGIN_LINK_CHR";
+ NSString* kBeginLinkOss = @"BEGIN_LINK_OSS";
+ NSString* kEndLinkChr = @"END_LINK_CHR";
+ NSString* kEndLinkOss = @"END_LINK_OSS";
+ // The CHR link should go to here
+ NSString* kChromiumProject = l10n_util::GetNSString(IDS_CHROMIUM_PROJECT_URL);
+ // The OSS link should go to here
+ NSString* kAcknowledgements =
+ [NSString stringWithUTF8String:chrome::kAboutCreditsURL];
+
+ // Now fetch the license string and deal with the markers
+
+ NSString* license =
+ l10n_util::GetNSStringWithFixup(IDS_ABOUT_VERSION_LICENSE);
+
+ NSRange begin_chr = [license rangeOfString:kBeginLinkChr];
+ NSRange begin_oss = [license rangeOfString:kBeginLinkOss];
+ NSRange end_chr = [license rangeOfString:kEndLinkChr];
+ NSRange end_oss = [license rangeOfString:kEndLinkOss];
+ DCHECK_NE(begin_chr.location, NSNotFound);
+ DCHECK_NE(begin_oss.location, NSNotFound);
+ DCHECK_NE(end_chr.location, NSNotFound);
+ DCHECK_NE(end_oss.location, NSNotFound);
+
+ // We don't know which link will come first, so we have to deal with things
+ // like this:
+ // [text][begin][text][end][text][start][text][end][text]
+
+ bool chromium_link_first = begin_chr.location < begin_oss.location;
+
+ NSRange* begin1 = &begin_chr;
+ NSRange* begin2 = &begin_oss;
+ NSRange* end1 = &end_chr;
+ NSRange* end2 = &end_oss;
+ NSString* link1 = kChromiumProject;
+ NSString* link2 = kAcknowledgements;
+ if (!chromium_link_first) {
+ // OSS came first, switch!
+ begin2 = &begin_chr;
+ begin1 = &begin_oss;
+ end2 = &end_chr;
+ end1 = &end_oss;
+ link2 = kChromiumProject;
+ link1 = kAcknowledgements;
+ }
+
+ NSString *sub_str;
+
+ AttributedStringAppendString(legal_block, @"\n");
+ sub_str = [license substringWithRange:NSMakeRange(0, begin1->location)];
+ AttributedStringAppendString(legal_block, sub_str);
+ sub_str = [license substringWithRange:NSMakeRange(NSMaxRange(*begin1),
+ end1->location -
+ NSMaxRange(*begin1))];
+ AttributedStringAppendHyperlink(legal_block, sub_str, link1);
+ sub_str = [license substringWithRange:NSMakeRange(NSMaxRange(*end1),
+ begin2->location -
+ NSMaxRange(*end1))];
+ AttributedStringAppendString(legal_block, sub_str);
+ sub_str = [license substringWithRange:NSMakeRange(NSMaxRange(*begin2),
+ end2->location -
+ NSMaxRange(*begin2))];
+ AttributedStringAppendHyperlink(legal_block, sub_str, link2);
+ sub_str = [license substringWithRange:NSMakeRange(NSMaxRange(*end2),
+ [license length] -
+ NSMaxRange(*end2))];
+ AttributedStringAppendString(legal_block, sub_str);
+
+#if defined(GOOGLE_CHROME_BUILD)
+ // Terms of service is only valid for Google Chrome
+
+ // The url within terms should point here:
+ NSString* kTOS = [NSString stringWithUTF8String:chrome::kAboutTermsURL];
+ // Following Windows. There is one marker in the string for where the terms
+ // link goes, but the text of the link comes from a second string resources.
+ std::vector<size_t> url_offsets;
+ NSString* about_terms = l10n_util::GetNSStringF(IDS_ABOUT_TERMS_OF_SERVICE,
+ string16(),
+ string16(),
+ &url_offsets);
+ DCHECK_EQ(url_offsets.size(), 1U);
+ NSString* terms_link_text =
+ l10n_util::GetNSStringWithFixup(IDS_TERMS_OF_SERVICE);
+
+ AttributedStringAppendString(legal_block, @"\n\n");
+ sub_str = [about_terms substringToIndex:url_offsets[0]];
+ AttributedStringAppendString(legal_block, sub_str);
+ AttributedStringAppendHyperlink(legal_block, terms_link_text, kTOS);
+ sub_str = [about_terms substringFromIndex:url_offsets[0]];
+ AttributedStringAppendString(legal_block, sub_str);
+#endif // GOOGLE_CHROME_BUILD
+
+ // We need to explicitly select Lucida Grande because once we click on
+ // the NSTextView, it changes to Helvetica 12 otherwise.
+ NSRange string_range = NSMakeRange(0, [legal_block length]);
+ [legal_block addAttribute:NSFontAttributeName
+ value:[NSFont labelFontOfSize:11]
+ range:string_range];
+
+ [legal_block endEditing];
+ return legal_block;
+}
+
+@end // @implementation AboutWindowController
diff --git a/chrome/browser/ui/cocoa/about_window_controller_unittest.mm b/chrome/browser/ui/cocoa/about_window_controller_unittest.mm
new file mode 100644
index 0000000..4747efe
--- /dev/null
+++ b/chrome/browser/ui/cocoa/about_window_controller_unittest.mm
@@ -0,0 +1,137 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/about_window_controller.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/keystone_glue.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+void PostAutoupdateStatusNotification(AutoupdateStatus status,
+ NSString* version) {
+ NSNumber* statusNumber = [NSNumber numberWithInt:status];
+ NSMutableDictionary* dictionary =
+ [NSMutableDictionary dictionaryWithObjects:&statusNumber
+ forKeys:&kAutoupdateStatusStatus
+ count:1];
+ if (version) {
+ [dictionary setObject:version forKey:kAutoupdateStatusVersion];
+ }
+
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center postNotificationName:kAutoupdateStatusNotification
+ object:nil
+ userInfo:dictionary];
+}
+
+class AboutWindowControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ about_window_controller_ =
+ [[AboutWindowController alloc] initWithProfile:nil];
+ EXPECT_TRUE([about_window_controller_ window]);
+ }
+
+ virtual void TearDown() {
+ [about_window_controller_ close];
+ CocoaTest::TearDown();
+ }
+
+ AboutWindowController* about_window_controller_;
+};
+
+TEST_F(AboutWindowControllerTest, TestCopyright) {
+ NSString* text = [[AboutWindowController legalTextBlock] string];
+
+ // Make sure we have the word "Copyright" in it, which is present in all
+ // locales.
+ NSRange range = [text rangeOfString:@"Copyright"];
+ EXPECT_NE(NSNotFound, range.location);
+}
+
+TEST_F(AboutWindowControllerTest, RemovesLinkAnchors) {
+ NSString* text = [[AboutWindowController legalTextBlock] string];
+
+ // Make sure that we removed the "BEGIN_LINK" and "END_LINK" anchors.
+ NSRange range = [text rangeOfString:@"BEGIN_LINK"];
+ EXPECT_EQ(NSNotFound, range.location);
+
+ range = [text rangeOfString:@"END_LINK"];
+ EXPECT_EQ(NSNotFound, range.location);
+}
+
+TEST_F(AboutWindowControllerTest, AwakeNibSetsString) {
+ NSAttributedString* legal_text = [AboutWindowController legalTextBlock];
+ NSAttributedString* text_storage =
+ [[about_window_controller_ legalText] textStorage];
+
+ EXPECT_TRUE([legal_text isEqualToAttributedString:text_storage]);
+}
+
+TEST_F(AboutWindowControllerTest, TestButton) {
+ NSButton* button = [about_window_controller_ updateButton];
+ ASSERT_TRUE(button);
+
+ // Not enabled until we know if updates are available.
+ ASSERT_FALSE([button isEnabled]);
+ PostAutoupdateStatusNotification(kAutoupdateAvailable, nil);
+ ASSERT_TRUE([button isEnabled]);
+
+ // Make sure the button is hooked up
+ ASSERT_EQ([button target], about_window_controller_);
+ ASSERT_EQ([button action], @selector(updateNow:));
+}
+
+// Doesn't confirm correctness, but does confirm something happens.
+TEST_F(AboutWindowControllerTest, TestCallbacks) {
+ NSString *lastText = [[about_window_controller_ updateText]
+ stringValue];
+ PostAutoupdateStatusNotification(kAutoupdateCurrent, @"foo");
+ ASSERT_NSNE(lastText, [[about_window_controller_ updateText] stringValue]);
+
+ lastText = [[about_window_controller_ updateText] stringValue];
+ PostAutoupdateStatusNotification(kAutoupdateCurrent, @"foo");
+ ASSERT_NSEQ(lastText, [[about_window_controller_ updateText] stringValue]);
+
+ lastText = [[about_window_controller_ updateText] stringValue];
+ PostAutoupdateStatusNotification(kAutoupdateCurrent, @"bar");
+ ASSERT_NSNE(lastText, [[about_window_controller_ updateText] stringValue]);
+
+ lastText = [[about_window_controller_ updateText] stringValue];
+ PostAutoupdateStatusNotification(kAutoupdateAvailable, nil);
+ ASSERT_NSNE(lastText, [[about_window_controller_ updateText] stringValue]);
+
+ lastText = [[about_window_controller_ updateText] stringValue];
+ PostAutoupdateStatusNotification(kAutoupdateCheckFailed, nil);
+ ASSERT_NSNE(lastText, [[about_window_controller_ updateText] stringValue]);
+
+#if 0
+ // TODO(mark): The kAutoupdateInstalled portion of the test is disabled
+ // because it leaks restart dialogs. If the About box is revised to use
+ // a button within the box to advise a restart instead of popping dialogs,
+ // these tests should be enabled.
+
+ lastText = [[about_window_controller_ updateText] stringValue];
+ PostAutoupdateStatusNotification(kAutoupdateInstalled, @"ver");
+ ASSERT_NSNE(lastText, [[about_window_controller_ updateText] stringValue]);
+
+ lastText = [[about_window_controller_ updateText] stringValue];
+ PostAutoupdateStatusNotification(kAutoupdateInstalled, nil);
+ ASSERT_NSNE(lastText, [[about_window_controller_ updateText] stringValue]);
+#endif
+
+ lastText = [[about_window_controller_ updateText] stringValue];
+ PostAutoupdateStatusNotification(kAutoupdateInstallFailed, nil);
+ ASSERT_NSNE(lastText, [[about_window_controller_ updateText] stringValue]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/accelerators_cocoa.h b/chrome/browser/ui/cocoa/accelerators_cocoa.h
new file mode 100644
index 0000000..b2d2269
--- /dev/null
+++ b/chrome/browser/ui/cocoa/accelerators_cocoa.h
@@ -0,0 +1,41 @@
+// 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_UI_COCOA_ACCELERATORS_COCOA_H_
+#define CHROME_BROWSER_UI_COCOA_ACCELERATORS_COCOA_H_
+#pragma once
+
+#include <map>
+
+#include "app/menus/accelerator_cocoa.h"
+
+// This class maintains a map of command_ids to AcceleratorCocoa objects (see
+// chrome/app/chrome_command_ids.h). Currently, this only lists the commands
+// that are used in the Wrench menu.
+//
+// It is recommended that this class be used as a singleton so that the key map
+// isn't created multiple places.
+//
+// #import "base/singleton.h"
+// ...
+// AcceleratorsCocoa* keymap = Singleton<AcceleratorsCocoa>::get();
+// return keymap->GetAcceleratorForCommand(IDC_COPY);
+//
+class AcceleratorsCocoa {
+ public:
+ AcceleratorsCocoa();
+ ~AcceleratorsCocoa() {}
+
+ typedef std::map<int, menus::AcceleratorCocoa> AcceleratorCocoaMap;
+
+ // Returns NULL if there is no accelerator for the command.
+ const menus::AcceleratorCocoa* GetAcceleratorForCommand(int command_id);
+
+ private:
+ AcceleratorCocoaMap accelerators_;
+
+ DISALLOW_COPY_AND_ASSIGN(AcceleratorsCocoa);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_ACCELERATORS_COCOA_H_
diff --git a/chrome/browser/ui/cocoa/accelerators_cocoa.mm b/chrome/browser/ui/cocoa/accelerators_cocoa.mm
new file mode 100644
index 0000000..eba1888
--- /dev/null
+++ b/chrome/browser/ui/cocoa/accelerators_cocoa.mm
@@ -0,0 +1,57 @@
+// 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 "chrome/browser/ui/cocoa/accelerators_cocoa.h"
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/app/chrome_command_ids.h"
+
+namespace {
+
+const struct AcceleratorMapping {
+ int command_id;
+ NSString* key;
+ NSUInteger modifiers;
+} kAcceleratorMap[] = {
+ { IDC_CLEAR_BROWSING_DATA, @"\x8", NSCommandKeyMask | NSShiftKeyMask },
+ { IDC_COPY, @"c", NSCommandKeyMask },
+ { IDC_CUT, @"x", NSCommandKeyMask },
+ { IDC_DEV_TOOLS, @"i", NSCommandKeyMask | NSAlternateKeyMask },
+ { IDC_DEV_TOOLS_CONSOLE, @"j", NSCommandKeyMask | NSAlternateKeyMask },
+ { IDC_FIND, @"f", NSCommandKeyMask },
+ { IDC_FULLSCREEN, @"f", NSCommandKeyMask | NSShiftKeyMask },
+ { IDC_NEW_INCOGNITO_WINDOW, @"n", NSCommandKeyMask | NSShiftKeyMask },
+ { IDC_NEW_TAB, @"t", NSCommandKeyMask },
+ { IDC_NEW_WINDOW, @"n", NSCommandKeyMask },
+ { IDC_OPTIONS, @",", NSCommandKeyMask },
+ { IDC_PASTE, @"v", NSCommandKeyMask },
+ { IDC_PRINT, @"p", NSCommandKeyMask },
+ { IDC_SAVE_PAGE, @"s", NSCommandKeyMask },
+ { IDC_SHOW_BOOKMARK_BAR, @"b", NSCommandKeyMask | NSShiftKeyMask },
+ { IDC_SHOW_BOOKMARK_MANAGER, @"b", NSCommandKeyMask | NSAlternateKeyMask },
+ { IDC_SHOW_DOWNLOADS, @"j", NSCommandKeyMask | NSShiftKeyMask },
+ { IDC_SHOW_HISTORY, @"y", NSCommandKeyMask },
+ { IDC_VIEW_SOURCE, @"u", NSCommandKeyMask | NSAlternateKeyMask },
+ { IDC_ZOOM_MINUS, @"-", NSCommandKeyMask },
+ { IDC_ZOOM_PLUS, @"+", NSCommandKeyMask }
+};
+
+} // namespace
+
+AcceleratorsCocoa::AcceleratorsCocoa() {
+ for (size_t i = 0; i < arraysize(kAcceleratorMap); ++i) {
+ const AcceleratorMapping& entry = kAcceleratorMap[i];
+ menus::AcceleratorCocoa accelerator(entry.key, entry.modifiers);
+ accelerators_.insert(std::make_pair(entry.command_id, accelerator));
+ }
+}
+
+const menus::AcceleratorCocoa* AcceleratorsCocoa::GetAcceleratorForCommand(
+ int command_id) {
+ AcceleratorCocoaMap::iterator it = accelerators_.find(command_id);
+ if (it == accelerators_.end())
+ return NULL;
+ return &it->second;
+}
diff --git a/chrome/browser/ui/cocoa/accelerators_cocoa_unittest.mm b/chrome/browser/ui/cocoa/accelerators_cocoa_unittest.mm
new file mode 100644
index 0000000..e2b27f0
--- /dev/null
+++ b/chrome/browser/ui/cocoa/accelerators_cocoa_unittest.mm
@@ -0,0 +1,28 @@
+// 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 "app/menus/accelerator_cocoa.h"
+#include "base/singleton.h"
+#include "chrome/app/chrome_command_ids.h"
+#import "chrome/browser/ui/cocoa/accelerators_cocoa.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/gtest_mac.h"
+
+TEST(AcceleratorsCocoaTest, GetAccelerator) {
+ AcceleratorsCocoa* keymap = Singleton<AcceleratorsCocoa>::get();
+ const menus::AcceleratorCocoa* accelerator =
+ keymap->GetAcceleratorForCommand(IDC_COPY);
+ ASSERT_TRUE(accelerator);
+ EXPECT_NSEQ(@"c", accelerator->characters());
+ EXPECT_EQ(static_cast<int>(NSCommandKeyMask), accelerator->modifiers());
+}
+
+TEST(AcceleratorsCocoaTest, GetNullAccelerator) {
+ AcceleratorsCocoa* keymap = Singleton<AcceleratorsCocoa>::get();
+ const menus::AcceleratorCocoa* accelerator =
+ keymap->GetAcceleratorForCommand(314159265);
+ EXPECT_FALSE(accelerator);
+}
diff --git a/chrome/browser/ui/cocoa/animatable_image.h b/chrome/browser/ui/cocoa/animatable_image.h
new file mode 100644
index 0000000..65fd023
--- /dev/null
+++ b/chrome/browser/ui/cocoa/animatable_image.h
@@ -0,0 +1,57 @@
+// 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_UI_COCOA_ANIMATABLE_IMAGE_H_
+#define CHROME_BROWSER_UI_COCOA_ANIMATABLE_IMAGE_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+#import <QuartzCore/QuartzCore.h>
+
+#include "base/scoped_nsobject.h"
+
+// This class helps animate an NSImage's frame and opacity. It works by creating
+// a blank NSWindow in the size specified and giving it a layer on which the
+// image can be animated. Clients are free to embed this object as a child
+// window for easier window management. This class will clean itself up when
+// the animation has finished. Clients that install this as a child window
+// should listen for the NSWindowWillCloseNotification to perform any additional
+// cleanup.
+@interface AnimatableImage : NSWindow {
+ @private
+ // The image to animate.
+ scoped_nsobject<NSImage> image_;
+
+ // The frame of the image before and after the animation. This is in this
+ // window's coordinate system.
+ CGRect startFrame_;
+ CGRect endFrame_;
+
+ // Opacity values for the animation.
+ CGFloat startOpacity_;
+ CGFloat endOpacity_;
+
+ // The amount of time it takes to animate the image.
+ CGFloat duration_;
+}
+
+@property (nonatomic) CGRect startFrame;
+@property (nonatomic) CGRect endFrame;
+@property (nonatomic) CGFloat startOpacity;
+@property (nonatomic) CGFloat endOpacity;
+@property (nonatomic) CGFloat duration;
+
+// Designated initializer. Do not use any other NSWindow initializers. Creates
+// but does not show the blank animation window of the given size. The
+// |animationFrame| should usually be big enough to contain the |startFrame|
+// and |endFrame| properties of the animation.
+- (id)initWithImage:(NSImage*)image
+ animationFrame:(NSRect)animationFrame;
+
+// Begins the animation.
+- (void)startAnimation;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_ANIMATABLE_IMAGE_H_
diff --git a/chrome/browser/ui/cocoa/animatable_image.mm b/chrome/browser/ui/cocoa/animatable_image.mm
new file mode 100644
index 0000000..a73f5a4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/animatable_image.mm
@@ -0,0 +1,145 @@
+// 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/ui/cocoa/animatable_image.h"
+
+#include "base/logging.h"
+#import "base/mac_util.h"
+#include "base/mac/scoped_cftyperef.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+
+@interface AnimatableImage (Private)
+- (void)setLayerContents:(CALayer*)layer;
+@end
+
+@implementation AnimatableImage
+
+@synthesize startFrame = startFrame_;
+@synthesize endFrame = endFrame_;
+@synthesize startOpacity = startOpacity_;
+@synthesize endOpacity = endOpacity_;
+@synthesize duration = duration_;
+
+- (id)initWithImage:(NSImage*)image
+ animationFrame:(NSRect)animationFrame {
+ if ((self = [super initWithContentRect:animationFrame
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:NO])) {
+ DCHECK(image);
+ image_.reset([image retain]);
+ duration_ = 1.0;
+ startOpacity_ = 1.0;
+ endOpacity_ = 1.0;
+
+ [self setOpaque:NO];
+ [self setBackgroundColor:[NSColor clearColor]];
+ [self setIgnoresMouseEvents:YES];
+
+ // Must be set or else self will be leaked.
+ [self setReleasedWhenClosed:YES];
+ }
+ return self;
+}
+
+- (void)startAnimation {
+ // Set up the root layer. By calling -setLayer: followed by -setWantsLayer:
+ // the view becomes a layer hosting view as opposed to a layer backed view.
+ NSView* view = [self contentView];
+ CALayer* rootLayer = [CALayer layer];
+ [view setLayer:rootLayer];
+ [view setWantsLayer:YES];
+
+ // Create the layer that will be animated.
+ CALayer* layer = [CALayer layer];
+ [self setLayerContents:layer];
+ [layer setAnchorPoint:CGPointMake(0, 1)];
+ [layer setFrame:[self startFrame]];
+ [layer setNeedsDisplayOnBoundsChange:YES];
+ [rootLayer addSublayer:layer];
+
+ // Common timing function for all animations.
+ CAMediaTimingFunction* mediaFunction =
+ [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
+
+ // Animate the bounds only if the image is resized.
+ CABasicAnimation* boundsAnimation = nil;
+ if (CGRectGetWidth([self startFrame]) != CGRectGetWidth([self endFrame]) ||
+ CGRectGetHeight([self startFrame]) != CGRectGetHeight([self endFrame])) {
+ boundsAnimation = [CABasicAnimation animationWithKeyPath:@"bounds"];
+ NSRect startRect = NSMakeRect(0, 0,
+ CGRectGetWidth([self startFrame]),
+ CGRectGetHeight([self startFrame]));
+ [boundsAnimation setFromValue:[NSValue valueWithRect:startRect]];
+ NSRect endRect = NSMakeRect(0, 0,
+ CGRectGetWidth([self endFrame]),
+ CGRectGetHeight([self endFrame]));
+ [boundsAnimation setToValue:[NSValue valueWithRect:endRect]];
+ [boundsAnimation gtm_setDuration:[self duration]
+ eventMask:NSLeftMouseUpMask];
+ [boundsAnimation setTimingFunction:mediaFunction];
+ }
+
+ // Positional animation.
+ CABasicAnimation* positionAnimation =
+ [CABasicAnimation animationWithKeyPath:@"position"];
+ [positionAnimation setFromValue:
+ [NSValue valueWithPoint:NSPointFromCGPoint([self startFrame].origin)]];
+ [positionAnimation setToValue:
+ [NSValue valueWithPoint:NSPointFromCGPoint([self endFrame].origin)]];
+ [positionAnimation gtm_setDuration:[self duration]
+ eventMask:NSLeftMouseUpMask];
+ [positionAnimation setTimingFunction:mediaFunction];
+
+ // Opacity animation.
+ CABasicAnimation* opacityAnimation =
+ [CABasicAnimation animationWithKeyPath:@"opacity"];
+ [opacityAnimation setFromValue:
+ [NSNumber numberWithFloat:[self startOpacity]]];
+ [opacityAnimation setToValue:[NSNumber numberWithFloat:[self endOpacity]]];
+ [opacityAnimation gtm_setDuration:[self duration]
+ eventMask:NSLeftMouseUpMask];
+ [opacityAnimation setTimingFunction:mediaFunction];
+ // Set the delegate just for one of the animations so that this window can
+ // be closed upon completion.
+ [opacityAnimation setDelegate:self];
+
+ // The CAAnimations only affect the presentational value of a layer, not the
+ // model value. This means that after the animation is done, it can flicker
+ // back to the original values. To avoid this, create an implicit animation of
+ // the values, which are then overridden with the CABasicAnimations.
+ //
+ // Ideally, a call to |-setBounds:| should be here, but, for reasons that
+ // are not understood, doing so causes the animation to break.
+ [layer setPosition:[self endFrame].origin];
+ [layer setOpacity:[self endOpacity]];
+
+ // Start the animations.
+ [CATransaction begin];
+ [CATransaction setValue:[NSNumber numberWithFloat:[self duration]]
+ forKey:kCATransactionAnimationDuration];
+ if (boundsAnimation) {
+ [layer addAnimation:boundsAnimation forKey:@"bounds"];
+ }
+ [layer addAnimation:positionAnimation forKey:@"position"];
+ [layer addAnimation:opacityAnimation forKey:@"opacity"];
+ [CATransaction commit];
+}
+
+// Sets the layer contents by converting the NSImage to a CGImageRef. This will
+// rasterize PDF resources.
+- (void)setLayerContents:(CALayer*)layer {
+ base::mac::ScopedCFTypeRef<CGImageRef> image(
+ mac_util::CopyNSImageToCGImage(image_.get()));
+ // Create the layer that will be animated.
+ [layer setContents:(id)image.get()];
+}
+
+// CAAnimation delegate method called when the animation is complete.
+- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)flag {
+ // Close the window, releasing self.
+ [self close];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/animatable_image_unittest.mm b/chrome/browser/ui/cocoa/animatable_image_unittest.mm
new file mode 100644
index 0000000..acdec8c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/animatable_image_unittest.mm
@@ -0,0 +1,46 @@
+// 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 "base/nsimage_cache_mac.h"
+#import "chrome/browser/ui/cocoa/animatable_image.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class AnimatableImageTest : public CocoaTest {
+ public:
+ AnimatableImageTest() {
+ NSRect frame = NSMakeRect(0, 0, 500, 500);
+ NSImage* image = nsimage_cache::ImageNamed(@"forward_Template.pdf");
+ animation_ = [[AnimatableImage alloc] initWithImage:image
+ animationFrame:frame];
+ }
+
+ AnimatableImage* animation_;
+};
+
+TEST_F(AnimatableImageTest, BasicAnimation) {
+ [animation_ setStartFrame:CGRectMake(0, 0, 10, 10)];
+ [animation_ setEndFrame:CGRectMake(500, 500, 100, 100)];
+ [animation_ setStartOpacity:0.1];
+ [animation_ setEndOpacity:1.0];
+ [animation_ setDuration:0.5];
+ [animation_ startAnimation];
+}
+
+TEST_F(AnimatableImageTest, CancelAnimation) {
+ [animation_ setStartFrame:CGRectMake(0, 0, 10, 10)];
+ [animation_ setEndFrame:CGRectMake(500, 500, 100, 100)];
+ [animation_ setStartOpacity:0.1];
+ [animation_ setEndOpacity:1.0];
+ [animation_ setDuration:5.0]; // Long enough to be able to test cancelling.
+ [animation_ startAnimation];
+ [animation_ close];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/animatable_view.h b/chrome/browser/ui/cocoa/animatable_view.h
new file mode 100644
index 0000000..dcf2c26
--- /dev/null
+++ b/chrome/browser/ui/cocoa/animatable_view.h
@@ -0,0 +1,59 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_ANIMATABLE_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_ANIMATABLE_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/background_gradient_view.h"
+#import "chrome/browser/ui/cocoa/view_resizer.h"
+
+// A view that provides an animatable height property. Provides methods to
+// animate to a new height, set a new height immediately, or cancel any running
+// animations.
+//
+// AnimatableView sends an |animationDidEnd:| message to its delegate when the
+// animation ends normally and an |animationDidStop:| message when the animation
+// was canceled (even when canceled as a result of a new animation starting).
+
+@interface AnimatableView : BackgroundGradientView<NSAnimationDelegate> {
+ @protected
+ IBOutlet id delegate_; // weak, used to send animation ended messages.
+
+ @private
+ scoped_nsobject<NSAnimation> currentAnimation_;
+ id<ViewResizer> resizeDelegate_; // weak, usually owns us
+}
+
+// Properties for bindings.
+@property(assign, nonatomic) id delegate;
+@property(assign, nonatomic) id<ViewResizer> resizeDelegate;
+
+// Gets the current height of the view. If an animation is currently running,
+// this will give the current height at the time of the call, not the target
+// height at the end of the animation.
+- (CGFloat)height;
+
+// Sets the height of the view immediately. Cancels any running animations.
+- (void)setHeight:(CGFloat)newHeight;
+
+// Starts a new animation to the given |newHeight| for the given |duration|.
+// Cancels any running animations.
+- (void)animateToNewHeight:(CGFloat)newHeight
+ duration:(NSTimeInterval)duration;
+
+// Cancels any running animations, leaving the view at its current
+// (mid-animation) height.
+- (void)stopAnimation;
+
+// Gets the progress of any current animation.
+- (NSAnimationProgress)currentAnimationProgress;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_ANIMATABLE_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/animatable_view.mm b/chrome/browser/ui/cocoa/animatable_view.mm
new file mode 100644
index 0000000..74dc4b1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/animatable_view.mm
@@ -0,0 +1,109 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+#import <QuartzCore/QuartzCore.h>
+
+#import "chrome/browser/ui/cocoa/animatable_view.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+
+// NSAnimation subclass that animates the height of an AnimatableView. Allows
+// the caller to start and cancel the animation as desired.
+@interface HeightAnimation : NSAnimation {
+ @private
+ AnimatableView* view_; // weak, owns us.
+ CGFloat startHeight_;
+ CGFloat endHeight_;
+}
+
+// Initialize a new height animation for the given view. The animation will not
+// start until startAnimation: is called.
+- (id)initWithView:(AnimatableView*)view
+ finalHeight:(CGFloat)height
+ duration:(NSTimeInterval)duration;
+@end
+
+@implementation HeightAnimation
+- (id)initWithView:(AnimatableView*)view
+ finalHeight:(CGFloat)height
+ duration:(NSTimeInterval)duration {
+ if ((self = [super gtm_initWithDuration:duration
+ eventMask:NSLeftMouseUpMask
+ animationCurve:NSAnimationEaseIn])) {
+ view_ = view;
+ startHeight_ = [view_ height];
+ endHeight_ = height;
+ [self setAnimationBlockingMode:NSAnimationNonblocking];
+ [self setDelegate:view_];
+ }
+ return self;
+}
+
+// Overridden to call setHeight for each progress tick.
+- (void)setCurrentProgress:(NSAnimationProgress)progress {
+ [super setCurrentProgress:progress];
+ [view_ setHeight:((progress * (endHeight_ - startHeight_)) + startHeight_)];
+}
+@end
+
+
+@implementation AnimatableView
+@synthesize delegate = delegate_;
+@synthesize resizeDelegate = resizeDelegate_;
+
+- (void)dealloc {
+ // Stop the animation if it is running, since it holds a pointer to this view.
+ [self stopAnimation];
+ [super dealloc];
+}
+
+- (CGFloat)height {
+ return [self frame].size.height;
+}
+
+- (void)setHeight:(CGFloat)newHeight {
+ // Force the height to be an integer because some animations look terrible
+ // with non-integer intermediate heights. We only ever set integer heights
+ // for our views, so this shouldn't be a limitation in practice.
+ int height = floor(newHeight);
+ [resizeDelegate_ resizeView:self newHeight:height];
+}
+
+- (void)animateToNewHeight:(CGFloat)newHeight
+ duration:(NSTimeInterval)duration {
+ [currentAnimation_ stopAnimation];
+
+ currentAnimation_.reset([[HeightAnimation alloc] initWithView:self
+ finalHeight:newHeight
+ duration:duration]);
+ if ([resizeDelegate_ respondsToSelector:@selector(setAnimationInProgress:)])
+ [resizeDelegate_ setAnimationInProgress:YES];
+ [currentAnimation_ startAnimation];
+}
+
+- (void)stopAnimation {
+ [currentAnimation_ stopAnimation];
+}
+
+- (NSAnimationProgress)currentAnimationProgress {
+ return [currentAnimation_ currentProgress];
+}
+
+- (void)animationDidStop:(NSAnimation*)animation {
+ if ([resizeDelegate_ respondsToSelector:@selector(setAnimationInProgress:)])
+ [resizeDelegate_ setAnimationInProgress:NO];
+ if ([delegate_ respondsToSelector:@selector(animationDidStop:)])
+ [delegate_ animationDidStop:animation];
+ currentAnimation_.reset(nil);
+}
+
+- (void)animationDidEnd:(NSAnimation*)animation {
+ if ([resizeDelegate_ respondsToSelector:@selector(setAnimationInProgress:)])
+ [resizeDelegate_ setAnimationInProgress:NO];
+ if ([delegate_ respondsToSelector:@selector(animationDidEnd:)])
+ [delegate_ animationDidEnd:animation];
+ currentAnimation_.reset(nil);
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/animatable_view_unittest.mm b/chrome/browser/ui/cocoa/animatable_view_unittest.mm
new file mode 100644
index 0000000..b9073a8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/animatable_view_unittest.mm
@@ -0,0 +1,48 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/animatable_view.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/view_resizer_pong.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class AnimatableViewTest : public CocoaTest {
+ public:
+ AnimatableViewTest() {
+ NSRect frame = NSMakeRect(0, 0, 100, 100);
+ view_.reset([[AnimatableView alloc] initWithFrame:frame]);
+ [[test_window() contentView] addSubview:view_.get()];
+
+ resizeDelegate_.reset([[ViewResizerPong alloc] init]);
+ [view_ setResizeDelegate:resizeDelegate_.get()];
+ }
+
+ scoped_nsobject<ViewResizerPong> resizeDelegate_;
+ scoped_nsobject<AnimatableView> view_;
+};
+
+// Basic view tests (AddRemove, Display).
+TEST_VIEW(AnimatableViewTest, view_);
+
+TEST_F(AnimatableViewTest, GetAndSetHeight) {
+ // Make sure the view's height starts out at 100.
+ NSRect initialFrame = [view_ frame];
+ ASSERT_EQ(100, initialFrame.size.height);
+ EXPECT_EQ(initialFrame.size.height, [view_ height]);
+
+ // Set it directly to 50 and make sure it takes effect.
+ [resizeDelegate_ setHeight:-1];
+ [view_ setHeight:50];
+ EXPECT_EQ(50, [resizeDelegate_ height]);
+}
+
+// TODO(rohitrao): Find a way to unittest the animations and delegate messages.
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/applescript/bookmark_applescript_utils_unittest.h b/chrome/browser/ui/cocoa/applescript/bookmark_applescript_utils_unittest.h
new file mode 100644
index 0000000..41e22d2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/bookmark_applescript_utils_unittest.h
@@ -0,0 +1,53 @@
+// 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_UI_COCOA_APPLESCRIPT_BOOKMARK_APPLESCRIPT_UTILS_UNITTEST_H_
+#define CHROME_BROWSER_UI_COCOA_APPLESCRIPT_BOOKMARK_APPLESCRIPT_UTILS_UNITTEST_H_
+
+#import <objc/objc-runtime.h>
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/app_controller_mac.h"
+#import "chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/test/model_test_utils.h"
+#include "testing/platform_test.h"
+
+class BookmarkModel;
+
+// The fake object that acts as our app's delegate, useful for testing purposes.
+@interface FakeAppDelegate : AppController {
+ @public
+ BrowserTestHelper* helper_; // weak.
+}
+@property (nonatomic) BrowserTestHelper* helper;
+// Return the |TestingProfile*| which is used for testing.
+- (Profile*)defaultProfile;
+@end
+
+
+// Used to emulate an active running script, useful for testing purposes.
+@interface FakeScriptCommand : NSScriptCommand {
+ Method originalMethod_;
+ Method alternateMethod_;
+}
+@end
+
+
+// The base class for all our bookmark releated unit tests.
+class BookmarkAppleScriptTest : public CocoaTest {
+ public:
+ BookmarkAppleScriptTest();
+ private:
+ BrowserTestHelper helper_;
+ scoped_nsobject<FakeAppDelegate> appDelegate_;
+ protected:
+ scoped_nsobject<BookmarkFolderAppleScript> bookmarkBar_;
+ BookmarkModel& model();
+};
+
+#endif
+// CHROME_BROWSER_UI_COCOA_APPLESCRIPT_BOOKMARK_APPLESCRIPT_UTILS_UNITTEST_H_
diff --git a/chrome/browser/ui/cocoa/applescript/bookmark_applescript_utils_unittest.mm b/chrome/browser/ui/cocoa/applescript/bookmark_applescript_utils_unittest.mm
new file mode 100644
index 0000000..108b1fb
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/bookmark_applescript_utils_unittest.mm
@@ -0,0 +1,62 @@
+// 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/ui/cocoa/applescript/bookmark_applescript_utils_unittest.h"
+
+#include "chrome/browser/bookmarks/bookmark_model.h"
+
+@implementation FakeAppDelegate
+
+@synthesize helper = helper_;
+
+- (Profile*)defaultProfile {
+ if (!helper_)
+ return NULL;
+ return helper_->profile();
+}
+@end
+
+// Represents the current fake command that is executing.
+static FakeScriptCommand* kFakeCurrentCommand;
+
+@implementation FakeScriptCommand
+
+- (id)init {
+ if ((self = [super init])) {
+ originalMethod_ = class_getClassMethod([NSScriptCommand class],
+ @selector(currentCommand));
+ alternateMethod_ = class_getClassMethod([self class],
+ @selector(currentCommand));
+ method_exchangeImplementations(originalMethod_, alternateMethod_);
+ kFakeCurrentCommand = self;
+ }
+ return self;
+}
+
++ (NSScriptCommand*)currentCommand {
+ return kFakeCurrentCommand;
+}
+
+- (void)dealloc {
+ method_exchangeImplementations(originalMethod_, alternateMethod_);
+ kFakeCurrentCommand = nil;
+ [super dealloc];
+}
+
+@end
+
+BookmarkAppleScriptTest::BookmarkAppleScriptTest() {
+ appDelegate_.reset([[FakeAppDelegate alloc] init]);
+ [appDelegate_.get() setHelper:&helper_];
+ [NSApp setDelegate:appDelegate_];
+ const BookmarkNode* root = model().GetBookmarkBarNode();
+ const std::string modelString("a f1:[ b d c ] d f2:[ e f g ] h ");
+ model_test_utils::AddNodesFromModelString(model(), root, modelString);
+ bookmarkBar_.reset([[BookmarkFolderAppleScript alloc]
+ initWithBookmarkNode:model().GetBookmarkBarNode()]);
+}
+
+BookmarkModel& BookmarkAppleScriptTest::model() {
+ return *helper_.profile()->GetBookmarkModel();
+}
diff --git a/chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript.h b/chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript.h
new file mode 100644
index 0000000..3e853b3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript.h
@@ -0,0 +1,70 @@
+// 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_UI_COCOA_APPLESCRIPT_BOOKMARK_FOLDER_APPLESCRIPT_H_
+#define CHROME_BROWSER_UI_COCOA_APPLESCRIPT_BOOKMARK_FOLDER_APPLESCRIPT_H_
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/applescript/bookmark_node_applescript.h"
+
+@class BookmarkItemAppleScript;
+
+// Represent a bookmark folder scriptable object in applescript.
+@interface BookmarkFolderAppleScript : BookmarkNodeAppleScript {
+
+}
+
+// Bookmark folder manipulation methods.
+// Returns an array of |BookmarkFolderAppleScript*| of all the bookmark folders
+// contained within this particular folder.
+- (NSArray*)bookmarkFolders;
+
+// Inserts a bookmark folder at the end.
+- (void)insertInBookmarkFolders:(id)aBookmarkFolder;
+
+// Inserts a bookmark folder at some position in the list.
+// Called by applescript which takes care of bounds checking, make sure of it
+// before calling directly.
+- (void)insertInBookmarkFolders:(id)aBookmarkFolder atIndex:(int)index;
+
+// Remove a bookmark folder from the list.
+// Called by applescript which takes care of bounds checking, make sure of it
+// before calling directly.
+- (void)removeFromBookmarkFoldersAtIndex:(int)index;
+
+// Bookmark item manipulation methods.
+// Returns an array of |BookmarkItemAppleScript*| of all the bookmark items
+// contained within this particular folder.
+- (NSArray*)bookmarkItems;
+
+// Inserts a bookmark item at the end.
+- (void)insertInBookmarkItems:(BookmarkItemAppleScript*)aBookmarkItem;
+
+// Inserts a bookmark item at some position in the list.
+// Called by applescript which takes care of bounds checking, make sure of it
+// before calling directly.
+- (void)insertInBookmarkItems:(BookmarkItemAppleScript*)aBookmarkItem
+ atIndex:(int)index;
+
+// Removes a bookmarks folder from the list.
+// Called by applescript which takes care of bounds checking, make sure of it
+// before calling directly.
+- (void)removeFromBookmarkItemsAtIndex:(int)index;
+
+// Returns the position of a bookmark folder within the current bookmark folder
+// which consists of bookmark folders as well as bookmark items.
+// AppleScript makes sure that there is a bookmark folder before calling this
+// method, make sure of that before calling directly.
+- (int)calculatePositionOfBookmarkFolderAt:(int)index;
+
+// Returns the position of a bookmark item within the current bookmark folder
+// which consists of bookmark folders as well as bookmark items.
+// AppleScript makes sure that there is a bookmark item before calling this
+// method, make sure of that before calling directly.
+- (int)calculatePositionOfBookmarkItemAt:(int)index;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_APPLESCRIPT_BOOKMARK_FOLDER_APPLESCRIPT_H_
diff --git a/chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript.mm b/chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript.mm
new file mode 100644
index 0000000..40d84b2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript.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 "chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript.h"
+
+#import "base/scoped_nsobject.h"
+#import "base/string16.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#import "chrome/browser/ui/cocoa/applescript/bookmark_item_applescript.h"
+#import "chrome/browser/ui/cocoa/applescript/constants_applescript.h"
+#include "chrome/browser/ui/cocoa/applescript/error_applescript.h"
+#include "googleurl/src/gurl.h"
+
+@implementation BookmarkFolderAppleScript
+
+- (NSArray*)bookmarkFolders {
+ NSMutableArray* bookmarkFolders = [NSMutableArray
+ arrayWithCapacity:bookmarkNode_->GetChildCount()];
+
+ for (int i = 0; i < bookmarkNode_->GetChildCount(); ++i) {
+ const BookmarkNode* node = bookmarkNode_->GetChild(i);
+
+ if (!node->is_folder())
+ continue;
+ scoped_nsobject<BookmarkFolderAppleScript> bookmarkFolder(
+ [[BookmarkFolderAppleScript alloc]
+ initWithBookmarkNode:node]);
+ [bookmarkFolder setContainer:self
+ property:AppleScript::kBookmarkFoldersProperty];
+ [bookmarkFolders addObject:bookmarkFolder];
+ }
+
+ return bookmarkFolders;
+}
+
+- (void)insertInBookmarkFolders:(id)aBookmarkFolder {
+ // This method gets called when a new bookmark folder is created so
+ // the container and property are set here.
+ [aBookmarkFolder setContainer:self
+ property:AppleScript::kBookmarkFoldersProperty];
+ BookmarkModel* model = [self bookmarkModel];
+ if (!model)
+ return;
+
+ const BookmarkNode* node = model->AddGroup(bookmarkNode_,
+ bookmarkNode_->GetChildCount(),
+ string16());
+ if (!node) {
+ AppleScript::SetError(AppleScript::errCreateBookmarkFolder);
+ return;
+ }
+
+ [aBookmarkFolder setBookmarkNode:node];
+}
+
+- (void)insertInBookmarkFolders:(id)aBookmarkFolder atIndex:(int)index {
+ // This method gets called when a new bookmark folder is created so
+ // the container and property are set here.
+ [aBookmarkFolder setContainer:self
+ property:AppleScript::kBookmarkFoldersProperty];
+ int position = [self calculatePositionOfBookmarkFolderAt:index];
+
+ BookmarkModel* model = [self bookmarkModel];
+ if (!model)
+ return;
+
+ const BookmarkNode* node = model->AddGroup(bookmarkNode_,
+ position,
+ string16());
+ if (!node) {
+ AppleScript::SetError(AppleScript::errCreateBookmarkFolder);
+ return;
+ }
+
+ [aBookmarkFolder setBookmarkNode:node];
+}
+
+- (void)removeFromBookmarkFoldersAtIndex:(int)index {
+ int position = [self calculatePositionOfBookmarkFolderAt:index];
+
+ BookmarkModel* model = [self bookmarkModel];
+ if (!model)
+ return;
+
+ model->Remove(bookmarkNode_, position);
+}
+
+- (NSArray*)bookmarkItems {
+ NSMutableArray* bookmarkItems = [NSMutableArray
+ arrayWithCapacity:bookmarkNode_->GetChildCount()];
+
+ for (int i = 0; i < bookmarkNode_->GetChildCount(); ++i) {
+ const BookmarkNode* node = bookmarkNode_->GetChild(i);
+
+ if (!node->is_url())
+ continue;
+ scoped_nsobject<BookmarkFolderAppleScript> bookmarkItem(
+ [[BookmarkItemAppleScript alloc]
+ initWithBookmarkNode:node]);
+ [bookmarkItem setContainer:self
+ property:AppleScript::kBookmarkItemsProperty];
+ [bookmarkItems addObject:bookmarkItem];
+ }
+
+ return bookmarkItems;
+}
+
+- (void)insertInBookmarkItems:(BookmarkItemAppleScript*)aBookmarkItem {
+ // This method gets called when a new bookmark item is created so
+ // the container and property are set here.
+ [aBookmarkItem setContainer:self
+ property:AppleScript::kBookmarkItemsProperty];
+
+ BookmarkModel* model = [self bookmarkModel];
+ if (!model)
+ return;
+
+ GURL url = GURL(base::SysNSStringToUTF8([aBookmarkItem URL]));
+ if (!url.is_valid()) {
+ AppleScript::SetError(AppleScript::errInvalidURL);
+ return;
+ }
+
+ const BookmarkNode* node = model->AddURL(bookmarkNode_,
+ bookmarkNode_->GetChildCount(),
+ string16(),
+ url);
+ if (!node) {
+ AppleScript::SetError(AppleScript::errCreateBookmarkItem);
+ return;
+ }
+
+ [aBookmarkItem setBookmarkNode:node];
+}
+
+- (void)insertInBookmarkItems:(BookmarkItemAppleScript*)aBookmarkItem
+ atIndex:(int)index {
+ // This method gets called when a new bookmark item is created so
+ // the container and property are set here.
+ [aBookmarkItem setContainer:self
+ property:AppleScript::kBookmarkItemsProperty];
+ int position = [self calculatePositionOfBookmarkItemAt:index];
+
+ BookmarkModel* model = [self bookmarkModel];
+ if (!model)
+ return;
+
+ GURL url(base::SysNSStringToUTF8([aBookmarkItem URL]));
+ if (!url.is_valid()) {
+ AppleScript::SetError(AppleScript::errInvalidURL);
+ return;
+ }
+
+ const BookmarkNode* node = model->AddURL(bookmarkNode_,
+ position,
+ string16(),
+ url);
+ if (!node) {
+ AppleScript::SetError(AppleScript::errCreateBookmarkItem);
+ return;
+ }
+
+ [aBookmarkItem setBookmarkNode:node];
+}
+
+- (void)removeFromBookmarkItemsAtIndex:(int)index {
+ int position = [self calculatePositionOfBookmarkItemAt:index];
+
+ BookmarkModel* model = [self bookmarkModel];
+ if (!model)
+ return;
+
+ model->Remove(bookmarkNode_, position);
+}
+
+- (int)calculatePositionOfBookmarkFolderAt:(int)index {
+ // Traverse through all the child nodes till the required node is found and
+ // return its position.
+ // AppleScript is 1-based therefore index is incremented by 1.
+ ++index;
+ int count = -1;
+ while (index) {
+ if (bookmarkNode_->GetChild(++count)->is_folder())
+ --index;
+ }
+ return count;
+}
+
+- (int)calculatePositionOfBookmarkItemAt:(int)index {
+ // Traverse through all the child nodes till the required node is found and
+ // return its position.
+ // AppleScript is 1-based therefore index is incremented by 1.
+ ++index;
+ int count = -1;
+ while (index) {
+ if (bookmarkNode_->GetChild(++count)->is_url())
+ --index;
+ }
+ return count;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript_unittest.mm b/chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript_unittest.mm
new file mode 100644
index 0000000..fbdbf09
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript_unittest.mm
@@ -0,0 +1,200 @@
+// 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"
+#include "base/sys_string_conversions.h"
+#import "chrome/browser/ui/cocoa/applescript/bookmark_applescript_utils_unittest.h"
+#import "chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript.h"
+#import "chrome/browser/ui/cocoa/applescript/bookmark_item_applescript.h"
+#import "chrome/browser/ui/cocoa/applescript/constants_applescript.h"
+#import "chrome/browser/ui/cocoa/applescript/error_applescript.h"
+#include "googleurl/src/gurl.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+typedef BookmarkAppleScriptTest BookmarkFolderAppleScriptTest;
+
+namespace {
+
+// Test all the bookmark folders within.
+TEST_F(BookmarkFolderAppleScriptTest, BookmarkFolders) {
+ NSArray* bookmarkFolders = [bookmarkBar_.get() bookmarkFolders];
+
+ EXPECT_EQ(2U, [bookmarkFolders count]);
+
+ BookmarkFolderAppleScript* f1 = [bookmarkFolders objectAtIndex:0];
+ BookmarkFolderAppleScript* f2 = [bookmarkFolders objectAtIndex:1];
+ EXPECT_NSEQ(@"f1", [f1 title]);
+ EXPECT_NSEQ(@"f2", [f2 title]);
+ EXPECT_EQ(2, [[f1 index] intValue]);
+ EXPECT_EQ(4, [[f2 index] intValue]);
+
+ for (BookmarkFolderAppleScript* bookmarkFolder in bookmarkFolders) {
+ EXPECT_EQ([bookmarkFolder container], bookmarkBar_.get());
+ EXPECT_NSEQ(AppleScript::kBookmarkFoldersProperty,
+ [bookmarkFolder containerProperty]);
+ }
+}
+
+// Insert a new bookmark folder.
+TEST_F(BookmarkFolderAppleScriptTest, InsertBookmarkFolder) {
+ // Emulate what applescript would do when inserting a new bookmark folder.
+ // Emulates a script like |set var to make new bookmark folder with
+ // properties {title:"foo"}|.
+ scoped_nsobject<BookmarkFolderAppleScript> bookmarkFolder(
+ [[BookmarkFolderAppleScript alloc] init]);
+ scoped_nsobject<NSNumber> var([[bookmarkFolder.get() uniqueID] copy]);
+ [bookmarkFolder.get() setTitle:@"foo"];
+ [bookmarkBar_.get() insertInBookmarkFolders:bookmarkFolder.get()];
+
+ // Represents the bookmark folder after its added.
+ BookmarkFolderAppleScript* bf =
+ [[bookmarkBar_.get() bookmarkFolders] objectAtIndex:2];
+ EXPECT_NSEQ(@"foo", [bf title]);
+ EXPECT_EQ([bf container], bookmarkBar_.get());
+ EXPECT_NSEQ(AppleScript::kBookmarkFoldersProperty,
+ [bf containerProperty]);
+ EXPECT_NSEQ(var.get(), [bf uniqueID]);
+}
+
+// Insert a new bookmark folder at a particular position.
+TEST_F(BookmarkFolderAppleScriptTest, InsertBookmarkFolderAtPosition) {
+ // Emulate what applescript would do when inserting a new bookmark folder.
+ // Emulates a script like |set var to make new bookmark folder with
+ // properties {title:"foo"} at after bookmark folder 1|.
+ scoped_nsobject<BookmarkFolderAppleScript> bookmarkFolder(
+ [[BookmarkFolderAppleScript alloc] init]);
+ scoped_nsobject<NSNumber> var([[bookmarkFolder.get() uniqueID] copy]);
+ [bookmarkFolder.get() setTitle:@"foo"];
+ [bookmarkBar_.get() insertInBookmarkFolders:bookmarkFolder.get() atIndex:1];
+
+ // Represents the bookmark folder after its added.
+ BookmarkFolderAppleScript* bf =
+ [[bookmarkBar_.get() bookmarkFolders] objectAtIndex:1];
+ EXPECT_NSEQ(@"foo", [bf title]);
+ EXPECT_EQ([bf container], bookmarkBar_.get());
+ EXPECT_NSEQ(AppleScript::kBookmarkFoldersProperty, [bf containerProperty]);
+ EXPECT_NSEQ(var.get(), [bf uniqueID]);
+}
+
+// Delete bookmark folders.
+TEST_F(BookmarkFolderAppleScriptTest, DeleteBookmarkFolders) {
+ unsigned int folderCount = 2, itemCount = 3;
+ for (unsigned int i = 0; i < folderCount; ++i) {
+ EXPECT_EQ(folderCount - i, [[bookmarkBar_.get() bookmarkFolders] count]);
+ EXPECT_EQ(itemCount, [[bookmarkBar_.get() bookmarkItems] count]);
+ [bookmarkBar_.get() removeFromBookmarkFoldersAtIndex:0];
+ }
+}
+
+// Test all the bookmark items within.
+TEST_F(BookmarkFolderAppleScriptTest, BookmarkItems) {
+ NSArray* bookmarkItems = [bookmarkBar_.get() bookmarkItems];
+
+ EXPECT_EQ(3U, [bookmarkItems count]);
+
+ BookmarkItemAppleScript* i1 = [bookmarkItems objectAtIndex:0];
+ BookmarkItemAppleScript* i2 = [bookmarkItems objectAtIndex:1];
+ BookmarkItemAppleScript* i3 = [bookmarkItems objectAtIndex:2];
+ EXPECT_NSEQ(@"a", [i1 title]);
+ EXPECT_NSEQ(@"d", [i2 title]);
+ EXPECT_NSEQ(@"h", [i3 title]);
+ EXPECT_EQ(1, [[i1 index] intValue]);
+ EXPECT_EQ(3, [[i2 index] intValue]);
+ EXPECT_EQ(5, [[i3 index] intValue]);
+
+ for (BookmarkItemAppleScript* bookmarkItem in bookmarkItems) {
+ EXPECT_EQ([bookmarkItem container], bookmarkBar_.get());
+ EXPECT_NSEQ(AppleScript::kBookmarkItemsProperty,
+ [bookmarkItem containerProperty]);
+ }
+}
+
+// Insert a new bookmark item.
+TEST_F(BookmarkFolderAppleScriptTest, InsertBookmarkItem) {
+ // Emulate what applescript would do when inserting a new bookmark folder.
+ // Emulates a script like |set var to make new bookmark item with
+ // properties {title:"Google", URL:"http://google.com"}|.
+ scoped_nsobject<BookmarkItemAppleScript> bookmarkItem(
+ [[BookmarkItemAppleScript alloc] init]);
+ scoped_nsobject<NSNumber> var([[bookmarkItem.get() uniqueID] copy]);
+ [bookmarkItem.get() setTitle:@"Google"];
+ [bookmarkItem.get() setURL:@"http://google.com"];
+ [bookmarkBar_.get() insertInBookmarkItems:bookmarkItem.get()];
+
+ // Represents the bookmark item after its added.
+ BookmarkItemAppleScript* bi =
+ [[bookmarkBar_.get() bookmarkItems] objectAtIndex:3];
+ EXPECT_NSEQ(@"Google", [bi title]);
+ EXPECT_EQ(GURL("http://google.com/"),
+ GURL(base::SysNSStringToUTF8([bi URL])));
+ EXPECT_EQ([bi container], bookmarkBar_.get());
+ EXPECT_NSEQ(AppleScript::kBookmarkItemsProperty, [bi containerProperty]);
+ EXPECT_NSEQ(var.get(), [bi uniqueID]);
+
+ // Test to see no bookmark item is created when no/invlid URL is entered.
+ scoped_nsobject<FakeScriptCommand> fakeScriptCommand(
+ [[FakeScriptCommand alloc] init]);
+ bookmarkItem.reset([[BookmarkItemAppleScript alloc] init]);
+ [bookmarkBar_.get() insertInBookmarkItems:bookmarkItem.get()];
+ EXPECT_EQ((int)AppleScript::errInvalidURL,
+ [fakeScriptCommand.get() scriptErrorNumber]);
+}
+
+// Insert a new bookmark item at a particular position.
+TEST_F(BookmarkFolderAppleScriptTest, InsertBookmarkItemAtPosition) {
+ // Emulate what applescript would do when inserting a new bookmark item.
+ // Emulates a script like |set var to make new bookmark item with
+ // properties {title:"XKCD", URL:"http://xkcd.org}
+ // at after bookmark item 1|.
+ scoped_nsobject<BookmarkItemAppleScript> bookmarkItem(
+ [[BookmarkItemAppleScript alloc] init]);
+ scoped_nsobject<NSNumber> var([[bookmarkItem.get() uniqueID] copy]);
+ [bookmarkItem.get() setTitle:@"XKCD"];
+ [bookmarkItem.get() setURL:@"http://xkcd.org"];
+
+ [bookmarkBar_.get() insertInBookmarkItems:bookmarkItem.get() atIndex:1];
+
+ // Represents the bookmark item after its added.
+ BookmarkItemAppleScript* bi =
+ [[bookmarkBar_.get() bookmarkItems] objectAtIndex:1];
+ EXPECT_NSEQ(@"XKCD", [bi title]);
+ EXPECT_EQ(GURL("http://xkcd.org/"),
+ GURL(base::SysNSStringToUTF8([bi URL])));
+ EXPECT_EQ([bi container], bookmarkBar_.get());
+ EXPECT_NSEQ(AppleScript::kBookmarkItemsProperty,
+ [bi containerProperty]);
+ EXPECT_NSEQ(var.get(), [bi uniqueID]);
+
+ // Test to see no bookmark item is created when no/invlid URL is entered.
+ scoped_nsobject<FakeScriptCommand> fakeScriptCommand(
+ [[FakeScriptCommand alloc] init]);
+ bookmarkItem.reset([[BookmarkItemAppleScript alloc] init]);
+ [bookmarkBar_.get() insertInBookmarkItems:bookmarkItem.get() atIndex:1];
+ EXPECT_EQ((int)AppleScript::errInvalidURL,
+ [fakeScriptCommand.get() scriptErrorNumber]);
+}
+
+// Delete bookmark items.
+TEST_F(BookmarkFolderAppleScriptTest, DeleteBookmarkItems) {
+ unsigned int folderCount = 2, itemCount = 3;
+ for (unsigned int i = 0; i < itemCount; ++i) {
+ EXPECT_EQ(folderCount, [[bookmarkBar_.get() bookmarkFolders] count]);
+ EXPECT_EQ(itemCount - i, [[bookmarkBar_.get() bookmarkItems] count]);
+ [bookmarkBar_.get() removeFromBookmarkItemsAtIndex:0];
+ }
+}
+
+// Set and get title.
+TEST_F(BookmarkFolderAppleScriptTest, GetAndSetTitle) {
+ NSArray* bookmarkFolders = [bookmarkBar_.get() bookmarkFolders];
+ BookmarkFolderAppleScript* folder1 = [bookmarkFolders objectAtIndex:0];
+ [folder1 setTitle:@"Foo"];
+ EXPECT_NSEQ(@"Foo", [folder1 title]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/applescript/bookmark_item_applescript.h b/chrome/browser/ui/cocoa/applescript/bookmark_item_applescript.h
new file mode 100644
index 0000000..b84365d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/bookmark_item_applescript.h
@@ -0,0 +1,33 @@
+// 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_UI_COCOA_APPLESCRIPT_BOOKMARK_ITEM_APPLESCRIPT_H_
+#define CHROME_BROWSER_UI_COCOA_APPLESCRIPT_BOOKMARK_ITEM_APPLESCRIPT_H_
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/applescript/bookmark_node_applescript.h"
+
+// Represents a bookmark item scriptable object in applescript.
+@interface BookmarkItemAppleScript : BookmarkNodeAppleScript {
+ @private
+ // Contains the temporary title when a user creates a new item with
+ // title specified like
+ // |make new bookmarks item with properties {title:"foo"}|.
+ NSString* tempURL_;
+}
+
+// Assigns a node, sets its unique ID and also copies temporary values.
+- (void)setBookmarkNode:(const BookmarkNode*)aBookmarkNode;
+
+// Returns the URL that the bookmark item holds.
+- (NSString*)URL;
+
+// Sets the URL of the bookmark item, displays error in applescript console
+// if URL is invalid.
+- (void)setURL:(NSString*)aURL;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_APPLESCRIPT_BOOKMARK_ITEM_APPLESCRIPT_H_
diff --git a/chrome/browser/ui/cocoa/applescript/bookmark_item_applescript.mm b/chrome/browser/ui/cocoa/applescript/bookmark_item_applescript.mm
new file mode 100644
index 0000000..9df8a31
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/bookmark_item_applescript.mm
@@ -0,0 +1,66 @@
+// 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/ui/cocoa/applescript/bookmark_item_applescript.h"
+
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/profile_manager.h"
+#import "chrome/browser/ui/cocoa/applescript/error_applescript.h"
+
+@interface BookmarkItemAppleScript()
+@property (nonatomic, copy) NSString* tempURL;
+@end
+
+@implementation BookmarkItemAppleScript
+
+@synthesize tempURL = tempURL_;
+
+- (id)init {
+ if ((self = [super init])) {
+ [self setTempURL:@""];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [tempURL_ release];
+ [super dealloc];
+}
+
+- (void)setBookmarkNode:(const BookmarkNode*)aBookmarkNode {
+ [super setBookmarkNode:aBookmarkNode];
+ [self setURL:[self tempURL]];
+}
+
+- (NSString*)URL {
+ if (!bookmarkNode_)
+ return tempURL_;
+
+ const GURL& url = bookmarkNode_->GetURL();
+ return base::SysUTF8ToNSString(url.spec());
+}
+
+- (void)setURL:(NSString*)aURL {
+ // If a scripter sets a URL before the node is added, URL is saved at a
+ // temporary location.
+ if (!bookmarkNode_) {
+ [self setTempURL:aURL];
+ return;
+ }
+
+ BookmarkModel* model = [self bookmarkModel];
+ if (!model)
+ return;
+
+ GURL url(base::SysNSStringToUTF8(aURL));
+ if (!url.is_valid()) {
+ AppleScript::SetError(AppleScript::errInvalidURL);
+ return;
+ }
+
+ model->SetURL(bookmarkNode_, url);
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/applescript/bookmark_item_applescript_unittest.mm b/chrome/browser/ui/cocoa/applescript/bookmark_item_applescript_unittest.mm
new file mode 100644
index 0000000..2cd5a94
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/bookmark_item_applescript_unittest.mm
@@ -0,0 +1,45 @@
+// 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"
+#include "base/sys_string_conversions.h"
+#import "chrome/browser/ui/cocoa/applescript/bookmark_applescript_utils_unittest.h"
+#import "chrome/browser/ui/cocoa/applescript/bookmark_item_applescript.h"
+#import "chrome/browser/ui/cocoa/applescript/error_applescript.h"
+#include "googleurl/src/gurl.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+typedef BookmarkAppleScriptTest BookmarkItemAppleScriptTest;
+
+namespace {
+
+// Set and get title.
+TEST_F(BookmarkItemAppleScriptTest, GetAndSetTitle) {
+ NSArray* bookmarkItems = [bookmarkBar_.get() bookmarkItems];
+ BookmarkItemAppleScript* item1 = [bookmarkItems objectAtIndex:0];
+ [item1 setTitle:@"Foo"];
+ EXPECT_NSEQ(@"Foo", [item1 title]);
+}
+
+// Set and get URL.
+TEST_F(BookmarkItemAppleScriptTest, GetAndSetURL) {
+ NSArray* bookmarkItems = [bookmarkBar_.get() bookmarkItems];
+ BookmarkItemAppleScript* item1 = [bookmarkItems objectAtIndex:0];
+ [item1 setURL:@"http://foo-bar.org"];
+ EXPECT_EQ(GURL("http://foo-bar.org"),
+ GURL(base::SysNSStringToUTF8([item1 URL])));
+
+ // If scripter enters invalid URL.
+ scoped_nsobject<FakeScriptCommand> fakeScriptCommand(
+ [[FakeScriptCommand alloc] init]);
+ [item1 setURL:@"invalid-url.org"];
+ EXPECT_EQ((int)AppleScript::errInvalidURL,
+ [fakeScriptCommand.get() scriptErrorNumber]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/applescript/bookmark_node_applescript.h b/chrome/browser/ui/cocoa/applescript/bookmark_node_applescript.h
new file mode 100644
index 0000000..0f1db68
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/bookmark_node_applescript.h
@@ -0,0 +1,48 @@
+// 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_UI_COCOA_APPLESCRIPT_BOOKMARK_NODE_APPLESCRIPT_H_
+#define CHROME_BROWSER_UI_COCOA_APPLESCRIPT_BOOKMARK_NODE_APPLESCRIPT_H_
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/applescript/element_applescript.h"
+
+class BookmarkModel;
+class BookmarkNode;
+
+// Contains all the elements that are common to both a bookmark folder and
+// bookmark item.
+@interface BookmarkNodeAppleScript : ElementAppleScript {
+ @protected
+ const BookmarkNode* bookmarkNode_; // weak.
+ // Contains the temporary title when a scripter creates a new folder/item with
+ // title specified like
+ // |make new bookmark folder with properties {title:"foo"}|.
+ NSString* tempTitle_;
+}
+
+// Does not actually create a folder/item but just sets its ID, the folder is
+// created in insertInBookmarksFolder: in the corresponding bookmarks folder.
+- (id)init;
+
+// Does not make a folder/item but instead uses an existing one.
+- (id)initWithBookmarkNode:(const BookmarkNode*)aBookmarkNode;
+
+// Assigns a node, sets its unique ID and also copies temporary values.
+- (void)setBookmarkNode:(const BookmarkNode*)aBookmarkNode;
+
+// Get and Set title.
+- (NSString*)title;
+- (void)setTitle:(NSString*)aTitle;
+
+// Returns the index with respect to its parent bookmark folder.
+- (NSNumber*)index;
+
+// Returns the bookmark model of the browser, returns NULL if there is an error.
+- (BookmarkModel*)bookmarkModel;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_APPLESCRIPT_BOOKMARK_NODE_APPLESCRIPT_H_
diff --git a/chrome/browser/ui/cocoa/applescript/bookmark_node_applescript.mm b/chrome/browser/ui/cocoa/applescript/bookmark_node_applescript.mm
new file mode 100644
index 0000000..dc0c14e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/bookmark_node_applescript.mm
@@ -0,0 +1,130 @@
+// 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/ui/cocoa/applescript/bookmark_node_applescript.h"
+
+#include "base/logging.h"
+#include "base/sys_string_conversions.h"
+#import "base/scoped_nsobject.h"
+#import "chrome/browser/app_controller_mac.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#import "chrome/browser/chrome_browser_application_mac.h"
+#include "chrome/browser/profile.h"
+#import "chrome/browser/ui/cocoa/applescript/bookmark_item_applescript.h"
+#import "chrome/browser/ui/cocoa/applescript/error_applescript.h"
+
+@interface BookmarkNodeAppleScript()
+@property (nonatomic, copy) NSString* tempTitle;
+@end
+
+@implementation BookmarkNodeAppleScript
+
+@synthesize tempTitle = tempTitle_;
+
+- (id)init {
+ if ((self = [super init])) {
+ BookmarkModel* model = [self bookmarkModel];
+ if (!model) {
+ [self release];
+ return nil;
+ }
+
+ scoped_nsobject<NSNumber> numID(
+ [[NSNumber alloc] initWithLongLong:model->next_node_id()]);
+ [self setUniqueID:numID];
+ [self setTempTitle:@""];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [tempTitle_ release];
+ [super dealloc];
+}
+
+
+- (id)initWithBookmarkNode:(const BookmarkNode*)aBookmarkNode {
+ if (!aBookmarkNode) {
+ [self release];
+ return nil;
+ }
+
+ if ((self = [super init])) {
+ // It is safe to be weak, if a bookmark item/folder goes away
+ // (eg user deleting a folder) the applescript runtime calls
+ // bookmarkFolders/bookmarkItems in BookmarkFolderAppleScript
+ // and this particular bookmark item/folder is never returned.
+ bookmarkNode_ = aBookmarkNode;
+
+ scoped_nsobject<NSNumber> numID(
+ [[NSNumber alloc] initWithLongLong:aBookmarkNode->id()]);
+ [self setUniqueID:numID];
+ }
+ return self;
+}
+
+- (void)setBookmarkNode:(const BookmarkNode*)aBookmarkNode {
+ DCHECK(aBookmarkNode);
+ // It is safe to be weak, if a bookmark item/folder goes away
+ // (eg user deleting a folder) the applescript runtime calls
+ // bookmarkFolders/bookmarkItems in BookmarkFolderAppleScript
+ // and this particular bookmark item/folder is never returned.
+ bookmarkNode_ = aBookmarkNode;
+
+ scoped_nsobject<NSNumber> numID(
+ [[NSNumber alloc] initWithLongLong:aBookmarkNode->id()]);
+ [self setUniqueID:numID];
+
+ [self setTitle:[self tempTitle]];
+}
+
+- (NSString*)title {
+ if (!bookmarkNode_)
+ return tempTitle_;
+
+ return base::SysUTF16ToNSString(bookmarkNode_->GetTitle());
+}
+
+- (void)setTitle:(NSString*)aTitle {
+ // If the scripter enters |make new bookmarks folder with properties
+ // {title:"foo"}|, the node has not yet been created so title is stored in the
+ // temp title.
+ if (!bookmarkNode_) {
+ [self setTempTitle:aTitle];
+ return;
+ }
+
+ BookmarkModel* model = [self bookmarkModel];
+ if (!model)
+ return;
+
+ model->SetTitle(bookmarkNode_, base::SysNSStringToUTF16(aTitle));
+}
+
+- (NSNumber*)index {
+ const BookmarkNode* parent = bookmarkNode_->GetParent();
+ int index = parent->IndexOfChild(bookmarkNode_);
+ // NOTE: AppleScript is 1-Based.
+ return [NSNumber numberWithInt:index+1];
+}
+
+- (BookmarkModel*)bookmarkModel {
+ AppController* appDelegate = [NSApp delegate];
+
+ Profile* defaultProfile = [appDelegate defaultProfile];
+ if (!defaultProfile) {
+ AppleScript::SetError(AppleScript::errGetProfile);
+ return NULL;
+ }
+
+ BookmarkModel* model = defaultProfile->GetBookmarkModel();
+ if (!model->IsLoaded()) {
+ AppleScript::SetError(AppleScript::errBookmarkModelLoad);
+ return NULL;
+ }
+
+ return model;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/applescript/browsercrapplication+applescript.h b/chrome/browser/ui/cocoa/applescript/browsercrapplication+applescript.h
new file mode 100644
index 0000000..795d198
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/browsercrapplication+applescript.h
@@ -0,0 +1,59 @@
+// 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_UI_COCOA_APPLESCRIPT_BROWSERCRAPPLICATION_APPLESCRIPT_H_
+#define CHROME_BROWSER_UI_COCOA_APPLESCRIPT_BROWSERCRAPPLICATION_APPLESCRIPT_H_
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/chrome_browser_application_mac.h"
+
+@class BookmarkFolderAppleScript;
+@class WindowAppleScript;
+
+// Represent the top level application scripting object in applescript.
+@interface BrowserCrApplication (AppleScriptAdditions)
+
+// Application window manipulation methods.
+// Returns an array of |WindowAppleScript*| of all windows present in the
+// application.
+- (NSArray*)appleScriptWindows;
+
+// Inserts a window at the beginning.
+- (void)insertInAppleScriptWindows:(WindowAppleScript*)aWindow;
+
+// Inserts a window at some position in the list.
+// Called by applescript which takes care of bounds checking, make sure of it
+// before calling directly.
+- (void)insertInAppleScriptWindows:(WindowAppleScript*)aWindow
+ atIndex:(int)index;
+
+// Removes a window from the list.
+// Called by applescript which takes care of bounds checking, make sure of it
+// before calling directly.
+- (void)removeFromAppleScriptWindowsAtIndex:(int)index;
+
+// Always returns nil to indicate that it is the root container object.
+- (NSScriptObjectSpecifier*)objectSpecifier;
+
+// Returns the other bookmarks bookmark folder,
+// returns nil if there is an error.
+- (BookmarkFolderAppleScript*)otherBookmarks;
+
+// Returns the bookmarks bar bookmark folder, return nil if there is an error.
+- (BookmarkFolderAppleScript*)bookmarksBar;
+
+// Returns the Bookmarks Bar and Other Bookmarks Folders, each is of type
+// |BookmarkFolderAppleScript*|.
+- (NSArray*)bookmarkFolders;
+
+// Required functions, even though bookmarkFolders is declared as
+// read-only, cocoa scripting does not currently prevent writing.
+- (void)insertInBookmarksFolders:(id)aBookmarkFolder;
+- (void)insertInBookmarksFolders:(id)aBookmarkFolder atIndex:(int)index;
+- (void)removeFromBookmarksFoldersAtIndex:(int)index;
+
+@end
+
+#endif// CHROME_BROWSER_UI_COCOA_APPLESCRIPT_BROWSERCRAPPLICATION_APPLESCRIPT_H_
diff --git a/chrome/browser/ui/cocoa/applescript/browsercrapplication+applescript.mm b/chrome/browser/ui/cocoa/applescript/browsercrapplication+applescript.mm
new file mode 100644
index 0000000..240ef0d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/browsercrapplication+applescript.mm
@@ -0,0 +1,136 @@
+// 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/ui/cocoa/applescript/browsercrapplication+applescript.h"
+
+#include "base/logging.h"
+#import "base/scoped_nsobject.h"
+#import "chrome/browser/app_controller_mac.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/browser_list.h"
+#include "chrome/browser/profile.h"
+#import "chrome/browser/ui/cocoa/applescript/bookmark_folder_applescript.h"
+#import "chrome/browser/ui/cocoa/applescript/constants_applescript.h"
+#import "chrome/browser/ui/cocoa/applescript/error_applescript.h"
+#import "chrome/browser/ui/cocoa/applescript/window_applescript.h"
+
+@implementation BrowserCrApplication (AppleScriptAdditions)
+
+- (NSArray*)appleScriptWindows {
+ NSMutableArray* appleScriptWindows = [NSMutableArray
+ arrayWithCapacity:BrowserList::size()];
+ // Iterate through all browsers and check if it closing,
+ // if not add it to list.
+ for (BrowserList::const_iterator browserIterator = BrowserList::begin();
+ browserIterator != BrowserList::end(); ++browserIterator) {
+ if ((*browserIterator)->IsAttemptingToCloseBrowser())
+ continue;
+
+ scoped_nsobject<WindowAppleScript> window(
+ [[WindowAppleScript alloc] initWithBrowser:*browserIterator]);
+ [window setContainer:NSApp
+ property:AppleScript::kWindowsProperty];
+ [appleScriptWindows addObject:window];
+ }
+ // Windows sorted by their index value, which is obtained by calling
+ // orderedIndex: on each window.
+ [appleScriptWindows sortUsingSelector:@selector(windowComparator:)];
+ return appleScriptWindows;
+}
+
+- (void)insertInAppleScriptWindows:(WindowAppleScript*)aWindow {
+ // This method gets called when a new window is created so
+ // the container and property are set here.
+ [aWindow setContainer:self
+ property:AppleScript::kWindowsProperty];
+}
+
+- (void)insertInAppleScriptWindows:(WindowAppleScript*)aWindow
+ atIndex:(int)index {
+ // This method gets called when a new window is created so
+ // the container and property are set here.
+ [aWindow setContainer:self
+ property:AppleScript::kWindowsProperty];
+ // Note: AppleScript is 1-based.
+ index--;
+ [aWindow setOrderedIndex:[NSNumber numberWithInt:index]];
+}
+
+- (void)removeFromAppleScriptWindowsAtIndex:(int)index {
+ [[[self appleScriptWindows] objectAtIndex:index]
+ handlesCloseScriptCommand:nil];
+}
+
+- (NSScriptObjectSpecifier*)objectSpecifier {
+ return nil;
+}
+
+- (BookmarkFolderAppleScript*)otherBookmarks {
+ AppController* appDelegate = [NSApp delegate];
+
+ Profile* defaultProfile = [appDelegate defaultProfile];
+ if (!defaultProfile) {
+ AppleScript::SetError(AppleScript::errGetProfile);
+ return nil;
+ }
+
+ BookmarkModel* model = defaultProfile->GetBookmarkModel();
+ if (!model->IsLoaded()) {
+ AppleScript::SetError(AppleScript::errBookmarkModelLoad);
+ return nil;
+ }
+
+ BookmarkFolderAppleScript* otherBookmarks =
+ [[[BookmarkFolderAppleScript alloc]
+ initWithBookmarkNode:model->other_node()] autorelease];
+ [otherBookmarks setContainer:self
+ property:AppleScript::kBookmarkFoldersProperty];
+ return otherBookmarks;
+}
+
+- (BookmarkFolderAppleScript*)bookmarksBar {
+ AppController* appDelegate = [NSApp delegate];
+
+ Profile* defaultProfile = [appDelegate defaultProfile];
+ if (!defaultProfile) {
+ AppleScript::SetError(AppleScript::errGetProfile);
+ return nil;
+ }
+
+ BookmarkModel* model = defaultProfile->GetBookmarkModel();
+ if (!model->IsLoaded()) {
+ AppleScript::SetError(AppleScript::errBookmarkModelLoad);
+ return NULL;
+ }
+
+ BookmarkFolderAppleScript* bookmarksBar =
+ [[[BookmarkFolderAppleScript alloc]
+ initWithBookmarkNode:model->GetBookmarkBarNode()] autorelease];
+ [bookmarksBar setContainer:self
+ property:AppleScript::kBookmarkFoldersProperty];
+ return bookmarksBar;
+}
+
+- (NSArray*)bookmarkFolders {
+ BookmarkFolderAppleScript* otherBookmarks = [self otherBookmarks];
+ BookmarkFolderAppleScript* bookmarksBar = [self bookmarksBar];
+ NSArray* folderArray = [NSArray arrayWithObjects:otherBookmarks,
+ bookmarksBar,
+ nil];
+ return folderArray;
+}
+
+- (void)insertInBookmarksFolders:(id)aBookmarkFolder {
+ NOTIMPLEMENTED();
+}
+
+- (void)insertInBookmarksFolders:(id)aBookmarkFolder atIndex:(int)index {
+ NOTIMPLEMENTED();
+}
+
+- (void)removeFromBookmarksFoldersAtIndex:(int)index {
+ NOTIMPLEMENTED();
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/applescript/browsercrapplication+applescript_test.mm b/chrome/browser/ui/cocoa/applescript/browsercrapplication+applescript_test.mm
new file mode 100644
index 0000000..a2e0f48
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/browsercrapplication+applescript_test.mm
@@ -0,0 +1,107 @@
+// 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 "chrome/browser/profile.h"
+#include "chrome/browser/ui/browser.h"
+#import "chrome/browser/ui/cocoa/applescript/browsercrapplication+applescript.h"
+#import "chrome/browser/ui/cocoa/applescript/constants_applescript.h"
+#import "chrome/browser/ui/cocoa/applescript/window_applescript.h"
+#include "chrome/test/in_process_browser_test.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/gtest_mac.h"
+
+typedef InProcessBrowserTest BrowserCrApplicationAppleScriptTest;
+
+// Create windows of different |Type|.
+IN_PROC_BROWSER_TEST_F(BrowserCrApplicationAppleScriptTest, Creation) {
+ // Create additional |Browser*| objects of different type.
+ Profile* profile = browser()->profile();
+ Browser* b1 = Browser::CreateForType(Browser::TYPE_POPUP, profile);
+ Browser* b2 = Browser::CreateForApp("", NULL, profile, true);
+ Browser* b3 = Browser::CreateForApp("", NULL, profile, false);
+
+ EXPECT_EQ(4U, [[NSApp appleScriptWindows] count]);
+ for (WindowAppleScript* window in [NSApp appleScriptWindows]) {
+ EXPECT_NSEQ(AppleScript::kWindowsProperty,
+ [window containerProperty]);
+ EXPECT_NSEQ(NSApp, [window container]);
+ }
+
+ // Close the additional browsers.
+ b1->CloseAllTabs();
+ b2->CloseAllTabs();
+ b3->CloseAllTabs();
+}
+
+// Insert a new window.
+IN_PROC_BROWSER_TEST_F(BrowserCrApplicationAppleScriptTest, InsertWindow) {
+ // Emulate what applescript would do when creating a new window.
+ // Emulate a script like |set var to make new window with properties
+ // {visible:false}|.
+ scoped_nsobject<WindowAppleScript> aWindow([[WindowAppleScript alloc] init]);
+ scoped_nsobject<NSNumber> var([[aWindow.get() uniqueID] copy]);
+ [aWindow.get() setValue:[NSNumber numberWithBool:YES] forKey:@"isVisible"];
+
+ [NSApp insertInAppleScriptWindows:aWindow.get()];
+
+ // Represents the window after it is added.
+ WindowAppleScript* window = [[NSApp appleScriptWindows] objectAtIndex:0];
+ EXPECT_NSEQ([NSNumber numberWithBool:YES],
+ [aWindow.get() valueForKey:@"isVisible"]);
+ EXPECT_EQ([window container], NSApp);
+ EXPECT_NSEQ(AppleScript::kWindowsProperty,
+ [window containerProperty]);
+ EXPECT_NSEQ(var, [window uniqueID]);
+}
+
+// Inserting and deleting windows.
+IN_PROC_BROWSER_TEST_F(BrowserCrApplicationAppleScriptTest,
+ InsertAndDeleteWindows) {
+ scoped_nsobject<WindowAppleScript> aWindow;
+ int count;
+ // Create a bunch of windows.
+ for (int i = 0; i < 5; ++i) {
+ for (int j = 0; j < 3; ++j) {
+ aWindow.reset([[WindowAppleScript alloc] init]);
+ [NSApp insertInAppleScriptWindows:aWindow.get()];
+ }
+ count = 3 * i + 4;
+ EXPECT_EQ(count, (int)[[NSApp appleScriptWindows] count]);
+ }
+
+ // Remove all the windows, just created.
+ count = (int)[[NSApp appleScriptWindows] count];
+ for (int i = 0; i < 5; ++i) {
+ for(int j = 0; j < 3; ++j) {
+ [NSApp removeFromAppleScriptWindowsAtIndex:0];
+ }
+ count = count - 3;
+ EXPECT_EQ(count, (int)[[NSApp appleScriptWindows] count]);
+ }
+}
+
+// Check for objectSpecifer of the root scripting object.
+IN_PROC_BROWSER_TEST_F(BrowserCrApplicationAppleScriptTest, ObjectSpecifier) {
+ // Should always return nil to indicate its the root scripting object.
+ EXPECT_EQ(nil, [NSApp objectSpecifier]);
+}
+
+// Bookmark folders at the root level.
+IN_PROC_BROWSER_TEST_F(BrowserCrApplicationAppleScriptTest, BookmarkFolders) {
+ NSArray* bookmarkFolders = [NSApp bookmarkFolders];
+ EXPECT_EQ(2U, [bookmarkFolders count]);
+
+ for (BookmarkFolderAppleScript* bookmarkFolder in bookmarkFolders) {
+ EXPECT_EQ(NSApp,
+ [bookmarkFolder container]);
+ EXPECT_NSEQ(AppleScript::kBookmarkFoldersProperty,
+ [bookmarkFolder containerProperty]);
+ }
+
+ EXPECT_NSEQ(@"Other Bookmarks", [[NSApp otherBookmarks] title]);
+ EXPECT_NSEQ(@"Bookmarks Bar", [[NSApp bookmarksBar] title]);
+}
+
diff --git a/chrome/browser/ui/cocoa/applescript/constants_applescript.h b/chrome/browser/ui/cocoa/applescript/constants_applescript.h
new file mode 100644
index 0000000..7ffa80e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/constants_applescript.h
@@ -0,0 +1,31 @@
+// 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_UI_COCOA_APPLESCRIPT_CONSTANTS_APPLESCRIPT_H_
+#define CHROME_BROWSER_UI_COCOA_APPLESCRIPT_CONSTANTS_APPLESCRIPT_H_
+
+#import <Cocoa/Cocoa.h>
+
+// This file contains the constant that are use to set the property of an
+// applescript scriptable item.
+namespace AppleScript {
+// Property to access windows.
+extern NSString* const kWindowsProperty;
+
+// Property to access tabs.
+extern NSString* const kTabsProperty;
+
+// Property to access bookmarks folders.
+extern NSString* const kBookmarkFoldersProperty;
+
+// Property to access bookmark items.
+extern NSString* const kBookmarkItemsProperty;
+
+// To indicate a window in normal mode.
+extern NSString* const kNormalWindowMode;
+
+// To indicate a window in incognito mode.
+extern NSString* const kIncognitoWindowMode;
+}
+#endif // CHROME_BROWSER_UI_COCOA_APPLESCRIPT_CONSTANTS_APPLESCRIPT_H_
diff --git a/chrome/browser/ui/cocoa/applescript/constants_applescript.mm b/chrome/browser/ui/cocoa/applescript/constants_applescript.mm
new file mode 100644
index 0000000..090077b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/constants_applescript.mm
@@ -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 "chrome/browser/ui/cocoa/applescript/constants_applescript.h"
+
+namespace AppleScript {
+// Property to access windows.
+NSString* const kWindowsProperty = @"appleScriptWindows";
+
+// Property to access tabs.
+NSString* const kTabsProperty = @"tabs";
+
+// Property to access bookmarks folders.
+NSString* const kBookmarkFoldersProperty = @"bookmarkFolders";
+
+// Property to access bookmark items.
+NSString* const kBookmarkItemsProperty = @"bookmarkItems";
+
+// To indicate a window in normal mode.
+NSString* const kNormalWindowMode = @"normal";
+
+// To indicate a window in incognito mode.
+NSString* const kIncognitoWindowMode = @"incognito";
+}
diff --git a/chrome/browser/ui/cocoa/applescript/element_applescript.h b/chrome/browser/ui/cocoa/applescript/element_applescript.h
new file mode 100644
index 0000000..49a3481
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/element_applescript.h
@@ -0,0 +1,37 @@
+// 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_UI_COCOA_APPLESCRIPT_ELEMENT_APPLESCRIPT_H_
+#define CHROME_BROWSER_UI_COCOA_APPLESCRIPT_ELEMENT_APPLESCRIPT_H_
+
+#import <Cocoa/Cocoa.h>
+
+// This class is the root class for all the other applescript classes.
+// It takes care of all the infrastructure type operations.
+@interface ElementAppleScript : NSObject {
+ @protected
+ // Used by the applescript runtime to identify each unique scriptable object.
+ NSNumber* uniqueID_;
+ // Used by object specifier to find a scriptable object's place in a
+ // collection.
+ id container_;
+ NSString* containerProperty_;
+}
+
+@property (nonatomic, copy) NSNumber* uniqueID;
+@property (nonatomic, retain) id container;
+@property (nonatomic, copy) NSString* containerProperty;
+
+// Calculates the objectspecifier by using the uniqueID, container and
+// container property.
+// An object specifier is used to identify objects within a
+// collection.
+- (NSScriptObjectSpecifier*)objectSpecifier;
+
+// Sets both container and property, retains container and copies property.
+- (void)setContainer:(id)value property:(NSString*)property;
+
+@end
+
+#endif// CHROME_BROWSER_UI_COCOA_APPLESCRIPT_ELEMENT_APPLESCRIPT_H_
diff --git a/chrome/browser/ui/cocoa/applescript/element_applescript.mm b/chrome/browser/ui/cocoa/applescript/element_applescript.mm
new file mode 100644
index 0000000..abaf01d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/element_applescript.mm
@@ -0,0 +1,38 @@
+// 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/ui/cocoa/applescript/element_applescript.h"
+
+@implementation ElementAppleScript
+
+@synthesize uniqueID = uniqueID_;
+@synthesize container = container_;
+@synthesize containerProperty = containerProperty_;
+
+// calling objectSpecifier asks an object to return an object specifier
+// record referring to itself. You must call setContainer:property: before
+// you can call this method.
+- (NSScriptObjectSpecifier*)objectSpecifier {
+ return [[NSUniqueIDSpecifier allocWithZone:[self zone]]
+ initWithContainerClassDescription:
+ (NSScriptClassDescription*)[[self container] classDescription]
+ containerSpecifier:
+ [[self container] objectSpecifier]
+ key:[self containerProperty]
+ uniqueID:[self uniqueID]];
+}
+
+- (void)setContainer:(id)value property:(NSString*)property {
+ [self setContainer:value];
+ [self setContainerProperty:property];
+}
+
+- (void)dealloc {
+ [uniqueID_ release];
+ [container_ release];
+ [containerProperty_ release];
+ [super dealloc];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/applescript/error_applescript.h b/chrome/browser/ui/cocoa/applescript/error_applescript.h
new file mode 100644
index 0000000..2b91a2c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/error_applescript.h
@@ -0,0 +1,41 @@
+// 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_UI_COCOA_APPLESCRIPT_ERROR_APPLESCRIPT_H_
+#define CHROME_BROWSER_UI_COCOA_APPLESCRIPT_ERROR_APPLESCRIPT_H_
+
+#import <Cocoa/Cocoa.h>
+
+namespace AppleScript {
+
+enum ErrorCode {
+ // Error when default profile cannot be obtained.
+ errGetProfile = 1,
+ // Error when bookmark model fails to load.
+ errBookmarkModelLoad,
+ // Error when bookmark folder cannot be created.
+ errCreateBookmarkFolder,
+ // Error when bookmark item cannot be created.
+ errCreateBookmarkItem,
+ // Error when URL entered is invalid.
+ errInvalidURL,
+ // Error when printing cannot be initiated.
+ errInitiatePrinting,
+ // Error when invalid tab save type is entered.
+ errInvalidSaveType,
+ // Error when invalid browser mode is entered.
+ errInvalidMode,
+ // Error when tab index is out of bounds.
+ errInvalidTabIndex,
+ // Error when mode is set after browser window is created.
+ errSetMode,
+ // Error when index of browser window is out of bounds.
+ errWrongIndex
+};
+
+// This function sets an error message to the currently executing command.
+void SetError(ErrorCode errorCode);
+}
+
+#endif // CHROME_BROWSER_UI_COCOA_APPLESCRIPT_ERROR_APPLESCRIPT_H_
diff --git a/chrome/browser/ui/cocoa/applescript/error_applescript.mm b/chrome/browser/ui/cocoa/applescript/error_applescript.mm
new file mode 100644
index 0000000..e86ffd0
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/error_applescript.mm
@@ -0,0 +1,56 @@
+// 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/ui/cocoa/applescript/error_applescript.h"
+
+#import "app/l10n_util_mac.h"
+#include "base/logging.h"
+#include "grit/generated_resources.h"
+
+void AppleScript::SetError(AppleScript::ErrorCode errorCode) {
+ using namespace l10n_util;
+ NSScriptCommand* current_command = [NSScriptCommand currentCommand];
+ [current_command setScriptErrorNumber:(int)errorCode];
+ NSString* error_string = @"";
+ switch (errorCode) {
+ case errGetProfile:
+ error_string = GetNSString(IDS_GET_PROFILE_ERROR_APPLESCRIPT_MAC);
+ break;
+ case errBookmarkModelLoad:
+ error_string = GetNSString(IDS_BOOKMARK_MODEL_LOAD_ERROR_APPLESCRIPT_MAC);
+ break;
+ case errCreateBookmarkFolder:
+ error_string =
+ GetNSString(IDS_CREATE_BOOKMARK_FOLDER_ERROR_APPLESCRIPT_MAC);
+ break;
+ case errCreateBookmarkItem:
+ error_string =
+ GetNSString(IDS_CREATE_BOOKMARK_ITEM_ERROR_APPLESCRIPT_MAC);
+ break;
+ case errInvalidURL:
+ error_string = GetNSString(IDS_INVALID_URL_APPLESCRIPT_MAC);
+ break;
+ case errInitiatePrinting:
+ error_string = GetNSString(IDS_INITIATE_PRINTING_ERROR_APPLESCRIPT_MAC);
+ break;
+ case errInvalidSaveType:
+ error_string = GetNSString(IDS_INVALID_SAVE_TYPE_ERROR_APPLESCRIPT_MAC);
+ break;
+ case errInvalidMode:
+ error_string = GetNSString(IDS_INVALID_MODE_ERROR_APPLESCRIPT_MAC);
+ break;
+ case errInvalidTabIndex:
+ error_string = GetNSString(IDS_INVALID_TAB_INDEX_APPLESCRIPT_MAC);
+ break;
+ case errSetMode:
+ error_string = GetNSString(IDS_SET_MODE_APPLESCRIPT_MAC);
+ break;
+ case errWrongIndex:
+ error_string = GetNSString(IDS_WRONG_INDEX_ERROR_APPLESCRIPT_MAC);
+ break;
+ default:
+ NOTREACHED();
+ }
+ [current_command setScriptErrorString:error_string];
+}
diff --git a/chrome/browser/ui/cocoa/applescript/examples/advanced_tab_manipulation.applescript b/chrome/browser/ui/cocoa/applescript/examples/advanced_tab_manipulation.applescript
new file mode 100644
index 0000000..d45bd86
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/examples/advanced_tab_manipulation.applescript
@@ -0,0 +1,24 @@
+-- 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.
+
+tell application "Chromium"
+ tell tab 1 of window 1
+ print -- Prints the tab, prompts the user for location.
+ end tell
+
+ tell tab 1 of window 1
+ save in "/Users/Foo/Documents/Google" as "only html"
+ -- Saves the contents of the tab without the accompanying resources.
+
+ save in "/Users/Foo/Documents/Google" as "complete html"
+ -- Saves the contents of the tab with the accompanying resources.
+
+ -- Note: both the |in| and |as| part are optional, without it user is
+ -- prompted for one.
+ end tell
+
+ tell tab 1 of window 1
+ view source -- View the HTML of the tab in a new tab.
+ end tell
+end tell
diff --git a/chrome/browser/ui/cocoa/applescript/examples/app_info.applescript b/chrome/browser/ui/cocoa/applescript/examples/app_info.applescript
new file mode 100644
index 0000000..8377e14
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/examples/app_info.applescript
@@ -0,0 +1,9 @@
+-- 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.
+
+-- Gets basic information about the app.
+tell application "Chromium"
+ set var1 to name
+ set var2 to version
+end tell
diff --git a/chrome/browser/ui/cocoa/applescript/examples/bookmark_current_tabs.applescript b/chrome/browser/ui/cocoa/applescript/examples/bookmark_current_tabs.applescript
new file mode 100644
index 0000000..6e88268
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/examples/bookmark_current_tabs.applescript
@@ -0,0 +1,23 @@
+-- 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.
+
+-- This script bookmarks the currently open tabs of a window.
+tell application "Chromium"
+ set url_list to {}
+ set title_list to {}
+ tell window 1
+ repeat with i from 1 to (count tabs)
+ set end of url_list to (URL of tab i)
+ set end of title_list to (title of tab i)
+ end repeat
+ end tell
+ tell bookmarks bar
+ set var to make new bookmark folder with properties {title:"New"}
+ tell var
+ repeat with i from 1 to (count url_list)
+ make new bookmark item with properties {URL:(item i of url_list), title:(item i of title_list)}
+ end repeat
+ end tell
+ end tell
+end tell \ No newline at end of file
diff --git a/chrome/browser/ui/cocoa/applescript/examples/copy_html.applescript b/chrome/browser/ui/cocoa/applescript/examples/copy_html.applescript
new file mode 100644
index 0000000..fd7b4e0
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/examples/copy_html.applescript
@@ -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.
+
+-- This script copies the HTML of a tab to a TextEdit document.
+tell application "Chromium"
+ tell tab 1 of window 1 to view source
+ repeat while (loading of tab 2 of window 1)
+ end repeat
+ tell tab 2 of window 1 to select all
+ tell tab 2 of window 1 to copy selection
+end tell
+
+tell application "TextEdit"
+ set text of document 1 to the clipboard
+end tell
diff --git a/chrome/browser/ui/cocoa/applescript/examples/delete_bookmarks.applescript b/chrome/browser/ui/cocoa/applescript/examples/delete_bookmarks.applescript
new file mode 100644
index 0000000..341b100
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/examples/delete_bookmarks.applescript
@@ -0,0 +1,13 @@
+-- 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.
+
+-- This script deletes all the items within a bookmark folder.
+tell application "Chromium"
+ set var to bookmark folder "New" of bookmarks bar
+ -- Change the folder to whichever you want.
+ tell var
+ delete every bookmark item
+ delete every bookmark folder
+ end tell
+end tell
diff --git a/chrome/browser/ui/cocoa/applescript/examples/execute_javascript.applescript b/chrome/browser/ui/cocoa/applescript/examples/execute_javascript.applescript
new file mode 100644
index 0000000..33a34b4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/examples/execute_javascript.applescript
@@ -0,0 +1,10 @@
+-- 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.
+
+-- This script execute a string of javascript code.
+tell application "Chromium"
+ tell tab 1 of window 1
+ execute javascript "alert('Hello World')"
+ end tell
+end tell
diff --git a/chrome/browser/ui/cocoa/applescript/examples/open_tabs_from_bookmark_folder.applescript b/chrome/browser/ui/cocoa/applescript/examples/open_tabs_from_bookmark_folder.applescript
new file mode 100644
index 0000000..af36fc1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/examples/open_tabs_from_bookmark_folder.applescript
@@ -0,0 +1,12 @@
+-- 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.
+
+tell application "Chromium"
+ set var to bookmark folder "New" of bookmarks bar
+ -- Change the folder to whichever you want.
+ repeat with i in (bookmark items of var)
+ set u to URL of i
+ tell window 1 to make new tab with properties {u}
+ end repeat
+end tell
diff --git a/chrome/browser/ui/cocoa/applescript/examples/quit_app.applescript b/chrome/browser/ui/cocoa/applescript/examples/quit_app.applescript
new file mode 100644
index 0000000..a6bdd1f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/examples/quit_app.applescript
@@ -0,0 +1,8 @@
+-- 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.
+
+-- Quits the application, useful in cases where you want to schedule things.
+tell application "Chromium"
+ quit
+end tell
diff --git a/chrome/browser/ui/cocoa/applescript/examples/tab_manipulation.applescript b/chrome/browser/ui/cocoa/applescript/examples/tab_manipulation.applescript
new file mode 100644
index 0000000..9038c17
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/examples/tab_manipulation.applescript
@@ -0,0 +1,45 @@
+-- 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.
+
+-- Contains some common tab manipulation commands.
+tell application "Chromium"
+ tell window 1 to make new tab with properties {URL:"http://google.com"}
+ -- create a new tab and navigate to a particular URL.
+
+ set var to active tab index of window 1
+ set active tab index of window 1 to (var - 1) -- Select the previous tab.
+
+ set var to active tab index of window 1
+ set active tab index of window 1 to (var + 1) -- Select the next tab.
+
+ get title of tab 1 of window 1 -- Get the URL that the user can see.
+
+ get loading of tab 1 of window 1 -- Check if a tab is loading.
+
+ -- Common edit/manipulation commands.
+ tell tab 1 of window 1
+ undo
+
+ redo
+
+ cut selection -- Cut a piece of text and place it on the system clipboard.
+
+ copy selection -- Copy a piece of text and place it on the system clipboard.
+
+ paste selection -- Paste a text from the system clipboard.
+
+ select all
+ end tell
+
+ -- Common navigation commands.
+ tell tab 1 of window 1
+ go back
+
+ go forward
+
+ reload
+
+ stop
+ end tell
+end tell
diff --git a/chrome/browser/ui/cocoa/applescript/examples/tab_navigation.applescript b/chrome/browser/ui/cocoa/applescript/examples/tab_navigation.applescript
new file mode 100644
index 0000000..2337869
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/examples/tab_navigation.applescript
@@ -0,0 +1,13 @@
+-- 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.
+
+tell application "Chromium"
+ tell window 1
+ -- creates a new tab and navigates to a particular URL.
+ make new tab with properties {URL:"http://google.com"}
+ -- Duplicate a tab.
+ set var to URL of tab 2
+ make new tab with properties {URL:var}
+ end tell
+end tell
diff --git a/chrome/browser/ui/cocoa/applescript/examples/window_creation.applescript b/chrome/browser/ui/cocoa/applescript/examples/window_creation.applescript
new file mode 100644
index 0000000..fc1486a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/examples/window_creation.applescript
@@ -0,0 +1,10 @@
+-- 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.
+
+-- creates 2 windows, one in normal mode and another in incognito mode.
+tell application "Chromium"
+ make new window
+ make new window with properties {mode:"incognito"}
+ count windows -- count how many windows are currently open.
+end tell
diff --git a/chrome/browser/ui/cocoa/applescript/examples/window_operations.applescript b/chrome/browser/ui/cocoa/applescript/examples/window_operations.applescript
new file mode 100644
index 0000000..90a0288
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/examples/window_operations.applescript
@@ -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.
+
+-- Contains usage of common window operations.
+tell application "Chromium"
+ get URL of active tab of window 1 -- The URL currently being seen.
+
+ set minimized of window 1 to true -- Minimizes a window.
+ set minimized of window 1 to false -- Maximizes a window.
+
+ get mode of window 1
+ -- Checks if a window is in |normal mode| or |incognito mode|
+
+ set visible of window 1 to true -- Hides a window.
+ set visible of window 1 to false -- UnHides a window.
+
+ -- Open multiple tabs.
+ set active tab index of window 1 to 2 -- Selects the second tab.
+
+
+end tell
diff --git a/chrome/browser/ui/cocoa/applescript/scripting.sdef b/chrome/browser/ui/cocoa/applescript/scripting.sdef
new file mode 100644
index 0000000..b67b2b7
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/scripting.sdef
@@ -0,0 +1,304 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
+<dictionary title="Dictionary">
+ <!--
+ STANDARD SUITE
+ -->
+ <suite name="Standard Suite" code="core" description="Common classes and commands for all applications.">
+ <cocoa name="NSCoreSuite"/>
+ <class name="application" code="capp" description="The application&apos;s top-level scripting object.">
+ <cocoa class="BrowserCrApplication"/>
+ <element description="The windows contained within this application, ordered front to back." type="window">
+ <cocoa key="appleScriptWindows"/>
+ </element>
+ <property name="name" code="pnam" description="The name of the application." type="text" access="r"/>
+ <property name="frontmost" code="pisf" description="Is this the frontmost (active) application?" type="boolean" access="r">
+ <cocoa key="isActive"/>
+ </property>
+ <property name="version" code="vers" description="The version of the application." type="text" access="r"/>
+ <responds-to command="quit">
+ <cocoa method="handleQuitScriptCommand:"/>
+ </responds-to>
+ </class>
+ <class name="window" code="cwin" description="A window.">
+ <cocoa class="WindowAppleScript"/>
+ <element description="The tabs contained within the window." type="tab">
+ <cocoa key="tabs"/>
+ </element>
+ <property name="name" code="pnam" description="The full title of the window." type="text" access="r">
+ <cocoa key="title"/>
+ </property>
+ <property name="id" code="ID " description="The unique identifier of the window." type="integer" access="r">
+ <cocoa key="uniqueID"/>
+ </property>
+ <property name="index" code="pidx" description="The index of the window, ordered front to back." type="integer">
+ <cocoa key="orderedIndex"/>
+ </property>
+ <property name="bounds" code="pbnd" description="The bounding rectangle of the window." type="rectangle">
+ <cocoa key="boundsAsQDRect"/>
+ </property>
+ <property name="closeable" code="hclb" description="Whether the window has a close box." type="boolean" access="r">
+ <cocoa key="hasCloseBox"/>
+ </property>
+ <property name="minimizable" code="ismn" description="Whether the window can be minimized." type="boolean" access="r">
+ <cocoa key="isMiniaturizable"/>
+ </property>
+ <property name="minimized" code="pmnd" description="Whether the window is currently minimized." type="boolean">
+ <cocoa key="isMiniaturized"/>
+ </property>
+ <property name="resizable" code="prsz" description="Whether the window can be resized." type="boolean" access="r">
+ <cocoa key="isResizable"/>
+ </property>
+ <property name="visible" code="pvis" description="Whether the window is currently visible." type="boolean">
+ <cocoa key="isVisible"/>
+ </property>
+ <property name="zoomable" code="iszm" description="Whether the window can be zoomed." type="boolean" access="r">
+ <cocoa key="isZoomable"/>
+ </property>
+ <property name="zoomed" code="pzum" description="Whether the window is currently zoomed." type="boolean">
+ <cocoa key="isZoomed"/>
+ </property>
+ <property name="active tab" code="acTa" description="Returns the currently selected tab" type="tab" access="r">
+ <cocoa key="activeTab"/>
+ </property>
+ <property name="mode" code="mode" description="Represents the mode of the window which can be &apos;normal&apos; or &apos;incognito&apos;, can be set only once during creation of the window." type="text">
+ <cocoa key="mode"/>
+ </property>
+ <property name="active tab index" code="acTI" description="The index of the active tab." type="integer"/>
+ <responds-to command="close">
+ <cocoa method="handlesCloseScriptCommand:"/>
+ </responds-to>
+ </class>
+ <command name="save" code="coresave" description="Save an object.">
+ <direct-parameter description="the object to save, usually a document or window" type="specifier"/>
+ <parameter name="in" code="kfil" description="The file in which to save the object." type="file" optional="yes">
+ <cocoa key="File"/>
+ </parameter>
+ <parameter name="as" code="fltp" description="The file type in which to save the data. Can be &apos;only html&apos; or &apos;complete html&apos;, default is &apos;complete html&apos;." type="text" optional="yes">
+ <cocoa key="FileType"/>
+ </parameter>
+ </command>
+ <!--
+ According to TN2106, 'open' should return the resulting document
+ object. However, the Cocoa implementation does not do this yet.
+ <result type="specifier"/>
+ -->
+ <command name="open" code="aevtodoc" description="Open a document.">
+ <direct-parameter description="The file(s) to be opened.">
+ <type type="file" list="yes"/>
+ </direct-parameter>
+ </command>
+ <command name="close" code="coreclos" description="Close a window.">
+ <cocoa class="NSCloseCommand"/>
+ <direct-parameter description="the document(s) or window(s) to close." type="specifier"/>
+ </command>
+ <command name="quit" code="aevtquit" description="Quit the application.">
+ <cocoa class="NSQuitCommand"/>
+ </command>
+ <command name="count" code="corecnte" description="Return the number of elements of a particular class within an object.">
+ <cocoa class="NSCountCommand"/>
+ <direct-parameter description="the object whose elements are to be counted" type="specifier"/>
+ <parameter name="each" code="kocl" description="The class of objects to be counted." type="type" optional="yes">
+ <cocoa key="ObjectClass"/>
+ </parameter>
+ <result description="the number of elements" type="integer"/>
+ </command>
+ <command name="delete" code="coredelo" description="Delete an object.">
+ <cocoa class="NSDeleteCommand"/>
+ <direct-parameter description="the object to delete" type="specifier"/>
+ </command>
+ <command name="duplicate" code="coreclon" description="Copy object(s) and put the copies at a new location.">
+ <cocoa class="NSCloneCommand"/>
+ <direct-parameter description="the object(s) to duplicate" type="specifier"/>
+ <parameter name="to" code="insh" description="The location for the new object(s)." type="location specifier" optional="yes">
+ <cocoa key="ToLocation"/>
+ </parameter>
+ <parameter name="with properties" code="prdt" description="Properties to be set in the new duplicated object(s)." type="record" optional="yes">
+ <cocoa key="WithProperties"/>
+ </parameter>
+ <result description="the duplicated object(s)" type="specifier"/>
+ </command>
+ <command name="exists" code="coredoex" description="Verify if an object exists.">
+ <cocoa class="NSExistsCommand"/>
+ <direct-parameter description="the object in question" type="any"/>
+ <result description="true if it exists, false if not" type="boolean"/>
+ </command>
+ <command name="make" code="corecrel" description="Make a new object.">
+ <cocoa class="NSCreateCommand"/>
+ <parameter name="new" code="kocl" description="The class of the new object." type="type">
+ <cocoa key="ObjectClass"/>
+ </parameter>
+ <parameter name="at" code="insh" description="The location at which to insert the object." type="location specifier" optional="yes">
+ <cocoa key="Location"/>
+ </parameter>
+ <parameter name="with data" code="data" description="The initial contents of the object." type="any" optional="yes">
+ <cocoa key="ObjectData"/>
+ </parameter>
+ <parameter name="with properties" code="prdt" description="The initial values for properties of the object." type="record" optional="yes">
+ <cocoa key="KeyDictionary"/>
+ </parameter>
+ <result description="to the new object" type="specifier"/>
+ </command>
+ <command name="move" code="coremove" description="Move object(s) to a new location.">
+ <cocoa class="NSMoveCommand"/>
+ <direct-parameter description="the object(s) to move" type="specifier"/>
+ <parameter name="to" code="insh" description="The new location for the object(s)." type="location specifier">
+ <cocoa key="ToLocation"/>
+ </parameter>
+ <result description="the moved object(s)" type="specifier"/>
+ </command>
+ <!-- NSCoreSuite doesn't define these.
+ <command name="run" code="aevtoapp" description="Run an application. Most applications will open an empty, untitled window."/>
+ <command name="reopen" code="aevtrapp" description="Reactivate a running application. Some applications will open a new untitled window if no window is open."/>
+ -->
+ <command name="print" code="aevtpdoc" description="Print an object.">
+ <!-- type would be better written as "file | document". -->
+ <direct-parameter description="The file(s) or document(s) to be printed." type="specifier"/>
+ </command>
+ <!-- "set" is supposed to be hidden. -->
+ <command name="set" code="coresetd" description="Set an object&apos;s data.">
+ <cocoa class="NSSetCommand"/>
+ <direct-parameter type="specifier"/>
+ <!-- "set" is supposed to return the fully evaluated "to" data.
+ <result type="any"/>
+ -->
+ <parameter name="to" code="data" description="The new value." type="any">
+ <cocoa key="Value"/>
+ </parameter>
+ </command>
+ <!-- "get" is supposed to be hidden. -->
+ <command name="get" code="coregetd" description="Get the data for an object.">
+ <cocoa class="NSGetCommand"/>
+ <direct-parameter type="specifier"/>
+ <result type="any"/>
+ </command>
+ </suite>
+ <suite name="Chromium Suite" code="CrSu" description="Common classes and commands for Chrome.">
+ <class-extension description="The application&apos;s top-level scripting object." extends="application">
+ <cocoa class="BrowserCrApplication"/>
+ <element description="Contains the bookmarks bar and other bookmarks folder." type="bookmark folder" access="r">
+ <cocoa key="bookmarkFolders"/>
+ </element>
+ <property name="bookmarks bar" code="ChBB" description="The bookmarks bar bookmark folder." type="bookmark folder" access="r">
+ <cocoa key="bookmarksBar"/>
+ </property>
+ <property name="other bookmarks" code="ChOB" description="The other bookmarks bookmark folder." type="bookmark folder" access="r">
+ <cocoa key="otherBookmarks"/>
+ </property>
+ </class-extension>
+ <class name="tab" code="CrTb" description="A tab.">
+ <cocoa class="TabAppleScript"/>
+ <property name="id" code="ID " description="Unique ID of the tab." type="integer" access="r">
+ <cocoa key="uniqueID"/>
+ </property>
+ <property name="title" code="pnam" description="The title of the tab." type="text" access="r"/>
+ <property name="URL" code="URL " description="The url visible to the user." type="text"/>
+ <property name="loading" code="ldng" description="Is loading?" type="boolean" access="r"/>
+ <responds-to command="undo">
+ <cocoa method="handlesUndoScriptCommand:"/>
+ </responds-to>
+ <responds-to command="redo">
+ <cocoa method="handlesRedoScriptCommand:"/>
+ </responds-to>
+ <responds-to command="cut selection">
+ <cocoa method="handlesCutScriptCommand:"/>
+ </responds-to>
+ <responds-to command="copy selection">
+ <cocoa method="handlesCopyScriptCommand:"/>
+ </responds-to>
+ <responds-to command="paste selection">
+ <cocoa method="handlesPasteScriptCommand:"/>
+ </responds-to>
+ <responds-to command="select all">
+ <cocoa method="handlesSelectAllScriptCommand:"/>
+ </responds-to>
+ <responds-to command="go back">
+ <cocoa method="handlesGoBackScriptCommand:"/>
+ </responds-to>
+ <responds-to command="go forward">
+ <cocoa method="handlesGoForwardScriptCommand:"/>
+ </responds-to>
+ <responds-to command="reload">
+ <cocoa method="handlesReloadScriptCommand:"/>
+ </responds-to>
+ <responds-to command="stop">
+ <cocoa method="handlesStopScriptCommand:"/>
+ </responds-to>
+ <responds-to command="print">
+ <cocoa method="handlesPrintScriptCommand:"/>
+ </responds-to>
+ <responds-to command="view source">
+ <cocoa method="handlesViewSourceScriptCommand:"/>
+ </responds-to>
+ <responds-to command="save">
+ <cocoa method="handlesSaveScriptCommand:"/>
+ </responds-to>
+ <responds-to command="execute">
+ <cocoa method="handlesExecuteJavascriptScriptCommand:"/>
+ </responds-to>
+ </class>
+ <class name="bookmark folder" code="CrBF" description="A bookmarks folder that contains other bookmarks folder and bookmark items.">
+ <cocoa class="BookmarkFolderAppleScript"/>
+ <element description="The bookmark folders present within." type="bookmark folder">
+ <cocoa key="bookmarkFolders"/>
+ </element>
+ <element description="The bookmarks present within." type="bookmark item">
+ <cocoa key="bookmarkItems"/>
+ </element>
+ <property name="id" code="ID " description="Unique ID of the bookmark folder." type="number" access="r">
+ <cocoa key="uniqueID"/>
+ </property>
+ <property name="title" code="pnam" description="The title of the folder." type="text"/>
+ <property name="index" code="indx" description="Returns the index with respect to its parent bookmark folder" type="number" access="r"/>
+ </class>
+ <class name="bookmark item" code="CrBI" description="An item consists of an URL and the title of a bookmark">
+ <cocoa class="BookmarkItemAppleScript"/>
+ <property name="id" code="ID " description="Unique ID of the bookmark item." type="integer" access="r">
+ <cocoa key="uniqueID"/>
+ </property>
+ <property name="title" code="pnam" description="The title of the bookmark item." type="text"/>
+ <property name="URL" code="URL " description="The URL of the bookmark." type="text"/>
+ <property name="index" code="indx" description="Returns the index with respect to its parent bookmark folder" type="number" access="r"/>
+ </class>
+ <command name="reload" code="CrSuRlod" description="Reload a tab.">
+ <direct-parameter description="The tab to execute the command in." type="specifier"/>
+ </command>
+ <command name="go back" code="CrSuBack" description="Go Back (If Possible).">
+ <direct-parameter description="The tab to execute the command in." type="specifier"/>
+ </command>
+ <command name="go forward" code="CrSuFwd " description="Go Forward (If Possible).">
+ <direct-parameter description="The tab to execute the command in." type="specifier"/>
+ </command>
+ <command name="select all" code="CrSuSlAl" description="Select all.">
+ <direct-parameter description="The tab to execute the command in." type="specifier"/>
+ </command>
+ <command name="cut selection" code="CrSuCut " description="Cut selected text (If Possible).">
+ <direct-parameter description="The tab to execute the command in." type="specifier"/>
+ </command>
+ <command name="copy selection" code="CrSuCop " description="Copy text.">
+ <direct-parameter description="The tab to execute the command in." type="specifier"/>
+ </command>
+ <command name="paste selection" code="CrSuPast" description="Paste text (If Possible).">
+ <direct-parameter description="The tab to execute the command in." type="specifier"/>
+ </command>
+ <command name="undo" code="CrSuUndo" description="Undo the last change.">
+ <direct-parameter description="The tab to execute the command in." type="specifier"/>
+ </command>
+ <command name="redo" code="CrSuRedo" description="Redo the last change.">
+ <direct-parameter description="The tab to execute the command in." type="specifier"/>
+ </command>
+ <command name="stop" code="CrSustop" description="Stop the current tab from loading.">
+ <direct-parameter description="The tab to execute the command in." type="specifier"/>
+ </command>
+ <command name="view source" code="CrSuVSrc" description="View the HTML source of the tab.">
+ <direct-parameter description="The tab to execute the command in." type="specifier"/>
+ </command>
+ <command name="execute" code="CrSuExJa" description="Execute a piece of javascript.">
+ <direct-parameter description="The tab to execute the command in." type="specifier"/>
+ <parameter name="javascript" code="JvSc" description="The javascript code to execute." type="text">
+ <cocoa key="javascript"/>
+ </parameter>
+ <result type="any"/>
+ </command>
+ </suite>
+</dictionary> \ No newline at end of file
diff --git a/chrome/browser/ui/cocoa/applescript/tab_applescript.h b/chrome/browser/ui/cocoa/applescript/tab_applescript.h
new file mode 100644
index 0000000..30064fa
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/tab_applescript.h
@@ -0,0 +1,79 @@
+// 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_UI_COCOA_APPLESCRIPT_TAB_APPLESCRIPT_H_
+#define CHROME_BROWSER_UI_COCOA_APPLESCRIPT_TAB_APPLESCRIPT_H_
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/applescript/element_applescript.h"
+
+class TabContents;
+
+// Represents a tab scriptable item in applescript.
+@interface TabAppleScript : ElementAppleScript {
+ @private
+ TabContents* tabContents_; // weak.
+ // Contains the temporary URL when a user creates a new folder/item with
+ // url specified like
+ // |make new tab with properties {url:"http://google.com"}|.
+ NSString* tempURL_;
+}
+
+// Doesn't actually create the tab here but just assigns the ID, tab is created
+// when it calls insertInTabs: of a particular window, it is used in cases
+// where user assigns a tab to a variable like |set var to make new tab|.
+- (id)init;
+
+// Does not create a new tab but uses an existing one.
+- (id)initWithTabContent:(TabContents*)aTabContent;
+
+// Assigns a tab, sets its unique ID and also copies temporary values.
+- (void)setTabContent:(TabContents*)aTabContent;
+
+// Return the URL currently visible to the user in the location bar.
+- (NSString*)URL;
+
+// Sets the URL, returns an error if it is invalid.
+- (void)setURL:(NSString*)aURL;
+
+// The title of the tab.
+- (NSString*)title;
+
+// Is the tab loading any resource?
+- (NSNumber*)loading;
+
+// Standard user commands.
+- (void)handlesUndoScriptCommand:(NSScriptCommand*)command;
+- (void)handlesRedoScriptCommand:(NSScriptCommand*)command;
+
+// Edit operations on the page.
+- (void)handlesCutScriptCommand:(NSScriptCommand*)command;
+- (void)handlesCopyScriptCommand:(NSScriptCommand*)command;
+- (void)handlesPasteScriptCommand:(NSScriptCommand*)command;
+
+// Selects all contents on the page.
+- (void)handlesSelectAllScriptCommand:(NSScriptCommand*)command;
+
+// Navigation operations.
+- (void)handlesGoBackScriptCommand:(NSScriptCommand*)command;
+- (void)handlesGoForwardScriptCommand:(NSScriptCommand*)command;
+- (void)handlesReloadScriptCommand:(NSScriptCommand*)command;
+- (void)handlesStopScriptCommand:(NSScriptCommand*)command;
+
+// Used to print a tab.
+- (void)handlesPrintScriptCommand:(NSScriptCommand*)command;
+
+// Used to save a tab, if no file is specified, prompts the user to enter it.
+- (void)handlesSaveScriptCommand:(NSScriptCommand*)command;
+
+// Displays the HTML of the tab in a new tab.
+- (void)handlesViewSourceScriptCommand:(NSScriptCommand*)command;
+
+// Executes a piece of javascript in the tab.
+- (id)handlesExecuteJavascriptScriptCommand:(NSScriptCommand*)command;
+
+@end
+
+#endif// CHROME_BROWSER_UI_COCOA_APPLESCRIPT_TAB_APPLESCRIPT_H_
diff --git a/chrome/browser/ui/cocoa/applescript/tab_applescript.mm b/chrome/browser/ui/cocoa/applescript/tab_applescript.mm
new file mode 100644
index 0000000..3a10095
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/tab_applescript.mm
@@ -0,0 +1,296 @@
+// 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/ui/cocoa/applescript/tab_applescript.h"
+
+#include "base/file_path.h"
+#include "base/logging.h"
+#import "base/scoped_nsobject.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/download/save_package.h"
+#include "chrome/browser/renderer_host/render_view_host.h"
+#include "chrome/browser/sessions/session_id.h"
+#include "chrome/browser/tab_contents/navigation_controller.h"
+#include "chrome/browser/tab_contents/navigation_entry.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/ui/cocoa/applescript/error_applescript.h"
+#include "chrome/common/url_constants.h"
+#include "googleurl/src/gurl.h"
+
+@interface TabAppleScript()
+@property (nonatomic, copy) NSString* tempURL;
+@end
+
+@implementation TabAppleScript
+
+@synthesize tempURL = tempURL_;
+
+- (id)init {
+ if ((self = [super init])) {
+ SessionID session;
+ SessionID::id_type futureSessionIDOfTab = session.id() + 1;
+ // Holds the SessionID that the new tab is going to get.
+ scoped_nsobject<NSNumber> numID(
+ [[NSNumber alloc]
+ initWithInt:futureSessionIDOfTab]);
+ [self setUniqueID:numID];
+ [self setTempURL:@""];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [tempURL_ release];
+ [super dealloc];
+}
+
+- (id)initWithTabContent:(TabContents*)aTabContent {
+ if (!aTabContent) {
+ [self release];
+ return nil;
+ }
+
+ if ((self = [super init])) {
+ // It is safe to be weak, if a tab goes away (eg user closing a tab)
+ // the applescript runtime calls tabs in AppleScriptWindow and this
+ // particular tab is never returned.
+ tabContents_ = aTabContent;
+ scoped_nsobject<NSNumber> numID(
+ [[NSNumber alloc]
+ initWithInt:tabContents_->controller().session_id().id()]);
+ [self setUniqueID:numID];
+ }
+ return self;
+}
+
+- (void)setTabContent:(TabContents*)aTabContent {
+ DCHECK(aTabContent);
+ // It is safe to be weak, if a tab goes away (eg user closing a tab)
+ // the applescript runtime calls tabs in AppleScriptWindow and this
+ // particular tab is never returned.
+ tabContents_ = aTabContent;
+ scoped_nsobject<NSNumber> numID(
+ [[NSNumber alloc]
+ initWithInt:tabContents_->controller().session_id().id()]);
+ [self setUniqueID:numID];
+
+ [self setURL:[self tempURL]];
+}
+
+- (NSString*)URL {
+ if (!tabContents_) {
+ return nil;
+ }
+
+ NavigationEntry* entry = tabContents_->controller().GetActiveEntry();
+ if (!entry) {
+ return nil;
+ }
+ const GURL& url = entry->virtual_url();
+ return base::SysUTF8ToNSString(url.spec());
+}
+
+- (void)setURL:(NSString*)aURL {
+ // If a scripter sets a URL before the node is added save it at a temporary
+ // location.
+ if (!tabContents_) {
+ [self setTempURL:aURL];
+ return;
+ }
+
+ GURL url(base::SysNSStringToUTF8(aURL));
+ // check for valid url.
+ if (!url.is_empty() && !url.is_valid()) {
+ AppleScript::SetError(AppleScript::errInvalidURL);
+ return;
+ }
+
+ NavigationEntry* entry = tabContents_->controller().GetActiveEntry();
+ if (!entry)
+ return;
+
+ const GURL& previousURL = entry->virtual_url();
+ tabContents_->OpenURL(url,
+ previousURL,
+ CURRENT_TAB,
+ PageTransition::TYPED);
+}
+
+- (NSString*)title {
+ NavigationEntry* entry = tabContents_->controller().GetActiveEntry();
+ if (!entry)
+ return nil;
+
+ std::wstring title;
+ if (entry != NULL) {
+ title = UTF16ToWideHack(entry->title());
+ }
+
+ return base::SysWideToNSString(title);
+}
+
+- (NSNumber*)loading {
+ BOOL loadingValue = tabContents_->is_loading() ? YES : NO;
+ return [NSNumber numberWithBool:loadingValue];
+}
+
+- (void)handlesUndoScriptCommand:(NSScriptCommand*)command {
+ RenderViewHost* view = tabContents_->render_view_host();
+ if (!view) {
+ NOTREACHED();
+ return;
+ }
+
+ view->Undo();
+}
+
+- (void)handlesRedoScriptCommand:(NSScriptCommand*)command {
+ RenderViewHost* view = tabContents_->render_view_host();
+ if (!view) {
+ NOTREACHED();
+ return;
+ }
+
+ view->Redo();
+}
+
+- (void)handlesCutScriptCommand:(NSScriptCommand*)command {
+ RenderViewHost* view = tabContents_->render_view_host();
+ if (!view) {
+ NOTREACHED();
+ return;
+ }
+
+ view->Cut();
+}
+
+- (void)handlesCopyScriptCommand:(NSScriptCommand*)command {
+ RenderViewHost* view = tabContents_->render_view_host();
+ if (!view) {
+ NOTREACHED();
+ return;
+ }
+
+ view->Copy();
+}
+
+- (void)handlesPasteScriptCommand:(NSScriptCommand*)command {
+ RenderViewHost* view = tabContents_->render_view_host();
+ if (!view) {
+ NOTREACHED();
+ return;
+ }
+
+ view->Paste();
+}
+
+- (void)handlesSelectAllScriptCommand:(NSScriptCommand*)command {
+ RenderViewHost* view = tabContents_->render_view_host();
+ if (!view) {
+ NOTREACHED();
+ return;
+ }
+
+ view->SelectAll();
+}
+
+- (void)handlesGoBackScriptCommand:(NSScriptCommand*)command {
+ NavigationController& navigationController = tabContents_->controller();
+ if (navigationController.CanGoBack())
+ navigationController.GoBack();
+}
+
+- (void)handlesGoForwardScriptCommand:(NSScriptCommand*)command {
+ NavigationController& navigationController = tabContents_->controller();
+ if (navigationController.CanGoForward())
+ navigationController.GoForward();
+}
+
+- (void)handlesReloadScriptCommand:(NSScriptCommand*)command {
+ NavigationController& navigationController = tabContents_->controller();
+ const bool checkForRepost = true;
+ navigationController.Reload(checkForRepost);
+}
+
+- (void)handlesStopScriptCommand:(NSScriptCommand*)command {
+ RenderViewHost* view = tabContents_->render_view_host();
+ if (!view) {
+ // We tolerate Stop being called even before a view has been created.
+ // So just log a warning instead of a NOTREACHED().
+ DLOG(WARNING) << "Stop: no view for handle ";
+ return;
+ }
+
+ view->Stop();
+}
+
+- (void)handlesPrintScriptCommand:(NSScriptCommand*)command {
+ bool initiateStatus = tabContents_->PrintNow();
+ if (initiateStatus == false) {
+ AppleScript::SetError(AppleScript::errInitiatePrinting);
+ }
+}
+
+- (void)handlesSaveScriptCommand:(NSScriptCommand*)command {
+ NSDictionary* dictionary = [command evaluatedArguments];
+
+ NSURL* fileURL = [dictionary objectForKey:@"File"];
+ // Scripter has not specifed the location at which to save, so we prompt for
+ // it.
+ if (!fileURL) {
+ tabContents_->OnSavePage();
+ return;
+ }
+
+ FilePath mainFile(base::SysNSStringToUTF8([fileURL path]));
+ // We create a directory path at the folder within which the file exists.
+ // Eg. if main_file = '/Users/Foo/Documents/Google.html'
+ // then directory_path = '/Users/Foo/Documents/Google_files/'.
+ FilePath directoryPath = mainFile.RemoveExtension();
+ directoryPath = directoryPath.InsertBeforeExtension(std::string("_files/"));
+
+ NSString* saveType = [dictionary objectForKey:@"FileType"];
+
+ SavePackage::SavePackageType savePackageType =
+ SavePackage::SAVE_AS_COMPLETE_HTML;
+ if (saveType) {
+ if ([saveType isEqualToString:@"only html"]) {
+ savePackageType = SavePackage::SAVE_AS_ONLY_HTML;
+ } else if ([saveType isEqualToString:@"complete html"]) {
+ savePackageType = SavePackage::SAVE_AS_COMPLETE_HTML;
+ } else {
+ AppleScript::SetError(AppleScript::errInvalidSaveType);
+ return;
+ }
+ }
+
+ tabContents_->SavePage(mainFile, directoryPath, savePackageType);
+}
+
+
+- (void)handlesViewSourceScriptCommand:(NSScriptCommand*)command {
+ NavigationEntry* entry = tabContents_->controller().GetLastCommittedEntry();
+ if (entry) {
+ tabContents_->OpenURL(GURL(chrome::kViewSourceScheme + std::string(":") +
+ entry->url().spec()), GURL(), NEW_FOREGROUND_TAB, PageTransition::LINK);
+ }
+}
+
+- (id)handlesExecuteJavascriptScriptCommand:(NSScriptCommand*)command {
+ RenderViewHost* view = tabContents_->render_view_host();
+ if (!view) {
+ NOTREACHED();
+ return nil;
+ }
+
+ std::wstring script = base::SysNSStringToWide(
+ [[command evaluatedArguments] objectForKey:@"javascript"]);
+ view->ExecuteJavascriptInWebFrame(L"", script);
+
+ // TODO(Shreyas): Figure out a way to get the response back.
+ return nil;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/applescript/window_applescript.h b/chrome/browser/ui/cocoa/applescript/window_applescript.h
new file mode 100644
index 0000000..6d98d10
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/window_applescript.h
@@ -0,0 +1,81 @@
+// 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_UI_COCOA_APPLESCRIPT_WINDOW_APPLESCRIPT_H_
+#define CHROME_BROWSER_UI_COCOA_APPLESCRIPT_WINDOW_APPLESCRIPT_H_
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/applescript/element_applescript.h"
+
+class Browser;
+class Profile;
+@class TabAppleScript;
+
+// Represents a window class.
+@interface WindowAppleScript : ElementAppleScript {
+ @private
+ Browser* browser_; // weak.
+}
+
+// Creates a new window, returns nil if there is an error.
+- (id)init;
+
+// Creates a new window with a particular profile.
+- (id)initWithProfile:(Profile*)aProfile;
+
+// Does not create a new window but uses an existing one.
+- (id)initWithBrowser:(Browser*)aBrowser;
+
+// Sets and gets the index of the currently selected tab.
+- (NSNumber*)activeTabIndex;
+- (void)setActiveTabIndex:(NSNumber*)anActiveTabIndex;
+
+// Mode refers to whether a window is a normal window or an incognito window
+// it can be set only once while creating the window.
+- (NSString*)mode;
+- (void)setMode:(NSString*)theMode;
+
+// Returns the currently selected tab.
+- (TabAppleScript*)activeTab;
+
+// Tab manipulation functions.
+// The tabs inside the window.
+// Returns |TabAppleScript*| of all the tabs contained
+// within this particular folder.
+- (NSArray*)tabs;
+
+// Insert a tab at the end.
+- (void)insertInTabs:(TabAppleScript*)aTab;
+
+// Insert a tab at some position in the list.
+// Called by applescript which takes care of bounds checking, make sure of it
+// before calling directly.
+- (void)insertInTabs:(TabAppleScript*)aTab atIndex:(int)index;
+
+// Remove a window from the list.
+// Called by applescript which takes care of bounds checking, make sure of it
+// before calling directly.
+- (void)removeFromTabsAtIndex:(int)index;
+
+// Set the index of a window.
+- (void)setOrderedIndex:(NSNumber*)anIndex;
+
+// Used to sort windows by index.
+- (NSComparisonResult)windowComparator:(WindowAppleScript*)otherWindow;
+
+// For standard window functions like zoomable, bounds etc, we dont handle it
+// but instead pass it onto the NSWindow associated with the window.
+- (id)valueForUndefinedKey:(NSString*)key;
+- (void)setValue:(id)value forUndefinedKey:(NSString*)key;
+
+// Used to close window.
+- (void)handlesCloseScriptCommand:(NSCloseCommand*)command;
+
+// The index of the window, windows are ordered front to back.
+- (NSNumber*)orderedIndex;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_APPLESCRIPT_WINDOW_APPLESCRIPT_H_
diff --git a/chrome/browser/ui/cocoa/applescript/window_applescript.mm b/chrome/browser/ui/cocoa/applescript/window_applescript.mm
new file mode 100644
index 0000000..d5c2fa9
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/window_applescript.mm
@@ -0,0 +1,246 @@
+// 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/ui/cocoa/applescript/window_applescript.h"
+
+#include "base/logging.h"
+#import "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "base/time.h"
+#import "chrome/browser/app_controller_mac.h"
+#import "chrome/browser/chrome_browser_application_mac.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents_wrapper.h"
+#include "chrome/browser/tabs/tab_strip_model.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/browser_navigator.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/ui/cocoa/applescript/constants_applescript.h"
+#include "chrome/browser/ui/cocoa/applescript/error_applescript.h"
+#import "chrome/browser/ui/cocoa/applescript/tab_applescript.h"
+#include "chrome/common/url_constants.h"
+
+@interface WindowAppleScript(WindowAppleScriptPrivateMethods)
+// The NSWindow that corresponds to this window.
+- (NSWindow*)nativeHandle;
+@end
+
+@implementation WindowAppleScript
+
+- (id)init {
+ // Check which mode to open a new window.
+ NSScriptCommand* command = [NSScriptCommand currentCommand];
+ NSString* mode = [[[command evaluatedArguments]
+ objectForKey:@"KeyDictionary"] objectForKey:@"mode"];
+ AppController* appDelegate = [NSApp delegate];
+
+ Profile* defaultProfile = [appDelegate defaultProfile];
+
+ if (!defaultProfile) {
+ AppleScript::SetError(AppleScript::errGetProfile);
+ return nil;
+ }
+
+ Profile* profile;
+ if ([mode isEqualToString:AppleScript::kIncognitoWindowMode]) {
+ profile = defaultProfile->GetOffTheRecordProfile();
+ }
+ else if ([mode isEqualToString:AppleScript::kNormalWindowMode] || !mode) {
+ profile = defaultProfile;
+ } else {
+ // Mode cannot be anything else
+ AppleScript::SetError(AppleScript::errInvalidMode);
+ return nil;
+ }
+ // Set the mode to nil, to ensure that it is not set once more.
+ [[[command evaluatedArguments] objectForKey:@"KeyDictionary"]
+ setValue:nil forKey:@"mode"];
+ return [self initWithProfile:profile];
+}
+
+- (id)initWithProfile:(Profile*)aProfile {
+ if (!aProfile) {
+ [self release];
+ return nil;
+ }
+
+ if ((self = [super init])) {
+ browser_ = Browser::Create(aProfile);
+ browser_->NewTab();
+ browser_->window()->Show();
+ scoped_nsobject<NSNumber> numID(
+ [[NSNumber alloc] initWithInt:browser_->session_id().id()]);
+ [self setUniqueID:numID];
+ }
+ return self;
+}
+
+- (id)initWithBrowser:(Browser*)aBrowser {
+ if (!aBrowser) {
+ [self release];
+ return nil;
+ }
+
+ if ((self = [super init])) {
+ // It is safe to be weak, if a window goes away (eg user closing a window)
+ // the applescript runtime calls appleScriptWindows in
+ // BrowserCrApplication and this particular window is never returned.
+ browser_ = aBrowser;
+ scoped_nsobject<NSNumber> numID(
+ [[NSNumber alloc] initWithInt:browser_->session_id().id()]);
+ [self setUniqueID:numID];
+ }
+ return self;
+}
+
+- (NSWindow*)nativeHandle {
+ // window() can be NULL during startup.
+ if (browser_->window())
+ return browser_->window()->GetNativeHandle();
+ return nil;
+}
+
+- (NSNumber*)activeTabIndex {
+ // Note: applescript is 1-based, that is lists begin with index 1.
+ int activeTabIndex = browser_->selected_index() + 1;
+ if (!activeTabIndex) {
+ return nil;
+ }
+ return [NSNumber numberWithInt:activeTabIndex];
+}
+
+- (void)setActiveTabIndex:(NSNumber*)anActiveTabIndex {
+ // Note: applescript is 1-based, that is lists begin with index 1.
+ int atIndex = [anActiveTabIndex intValue] - 1;
+ if (atIndex >= 0 && atIndex < browser_->tab_count())
+ browser_->SelectTabContentsAt(atIndex, true);
+ else
+ AppleScript::SetError(AppleScript::errInvalidTabIndex);
+}
+
+- (NSString*)mode {
+ Profile* profile = browser_->profile();
+ if (profile->IsOffTheRecord())
+ return AppleScript::kIncognitoWindowMode;
+ return AppleScript::kNormalWindowMode;
+}
+
+- (void)setMode:(NSString*)theMode {
+ // cannot set mode after window is created.
+ if (theMode) {
+ AppleScript::SetError(AppleScript::errSetMode);
+ }
+}
+
+- (TabAppleScript*)activeTab {
+ TabAppleScript* currentTab = [[[TabAppleScript alloc]
+ initWithTabContent:browser_->GetSelectedTabContents()] autorelease];
+ [currentTab setContainer:self
+ property:AppleScript::kTabsProperty];
+ return currentTab;
+}
+
+- (NSArray*)tabs {
+ NSMutableArray* tabs = [NSMutableArray
+ arrayWithCapacity:browser_->tab_count()];
+
+ for (int i = 0; i < browser_->tab_count(); ++i) {
+ // Check to see if tab is closing.
+ if (browser_->GetTabContentsAt(i)->is_being_destroyed()) {
+ continue;
+ }
+
+ scoped_nsobject<TabAppleScript> tab(
+ [[TabAppleScript alloc]
+ initWithTabContent:(browser_->GetTabContentsAt(i))]);
+ [tab setContainer:self
+ property:AppleScript::kTabsProperty];
+ [tabs addObject:tab];
+ }
+ return tabs;
+}
+
+- (void)insertInTabs:(TabAppleScript*)aTab {
+ // This method gets called when a new tab is created so
+ // the container and property are set here.
+ [aTab setContainer:self
+ property:AppleScript::kTabsProperty];
+
+ // Set how long it takes a tab to be created.
+ base::TimeTicks newTabStartTime = base::TimeTicks::Now();
+ TabContentsWrapper* contents =
+ browser_->AddSelectedTabWithURL(GURL(chrome::kChromeUINewTabURL),
+ PageTransition::TYPED);
+ contents->tab_contents()->set_new_tab_start_time(newTabStartTime);
+ [aTab setTabContent:contents->tab_contents()];
+}
+
+- (void)insertInTabs:(TabAppleScript*)aTab atIndex:(int)index {
+ // This method gets called when a new tab is created so
+ // This method gets called when a new tab is created so
+ // the container and property are set here.
+ [aTab setContainer:self
+ property:AppleScript::kTabsProperty];
+
+ // Set how long it takes a tab to be created.
+ base::TimeTicks newTabStartTime = base::TimeTicks::Now();
+ browser::NavigateParams params(browser_,
+ GURL(chrome::kChromeUINewTabURL),
+ PageTransition::TYPED);
+ params.disposition = NEW_FOREGROUND_TAB;
+ params.tabstrip_index = index;
+ browser::Navigate(&params);
+ params.target_contents->tab_contents()->set_new_tab_start_time(
+ newTabStartTime);
+
+ [aTab setTabContent:params.target_contents->tab_contents()];
+}
+
+- (void)removeFromTabsAtIndex:(int)index {
+ browser_->tabstrip_model()->DetachTabContentsAt(index);
+}
+
+- (NSNumber*)orderedIndex{
+ return [NSNumber numberWithInt:[[self nativeHandle] orderedIndex]];
+}
+
+- (void)setOrderedIndex:(NSNumber*)anIndex {
+ int index = [anIndex intValue] - 1;
+ if (index < 0 || index >= (int)BrowserList::size()) {
+ AppleScript::SetError(AppleScript::errWrongIndex);
+ return;
+ }
+ [[self nativeHandle] setOrderedIndex:index];
+}
+
+- (NSComparisonResult)windowComparator:(WindowAppleScript*)otherWindow {
+ int thisIndex = [[self orderedIndex] intValue];
+ int otherIndex = [[otherWindow orderedIndex] intValue];
+ if (thisIndex < otherIndex)
+ return NSOrderedAscending;
+ else if (thisIndex > otherIndex)
+ return NSOrderedDescending;
+ // Indexes can never be same.
+ NOTREACHED();
+ return NSOrderedSame;
+}
+
+// Get and set values from the associated NSWindow.
+- (id)valueForUndefinedKey:(NSString*)key {
+ return [[self nativeHandle] valueForKey:key];
+}
+
+- (void)setValue:(id)value forUndefinedKey:(NSString*)key {
+ [[self nativeHandle] setValue:(id)value forKey:key];
+}
+
+- (void)handlesCloseScriptCommand:(NSCloseCommand*)command {
+ // window() can be NULL during startup.
+ if (browser_->window())
+ browser_->window()->Close();
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/applescript/window_applescript_test.mm b/chrome/browser/ui/cocoa/applescript/window_applescript_test.mm
new file mode 100644
index 0000000..b40e5d5
--- /dev/null
+++ b/chrome/browser/ui/cocoa/applescript/window_applescript_test.mm
@@ -0,0 +1,178 @@
+// 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 "base/scoped_nsobject.h"
+#include "base/sys_string_conversions.h"
+#import "chrome/browser/app_controller_mac.h"
+#import "chrome/browser/chrome_browser_application_mac.h"
+#include "chrome/browser/profile.h"
+#import "chrome/browser/ui/cocoa/applescript/constants_applescript.h"
+#import "chrome/browser/ui/cocoa/applescript/error_applescript.h"
+#import "chrome/browser/ui/cocoa/applescript/tab_applescript.h"
+#import "chrome/browser/ui/cocoa/applescript/window_applescript.h"
+#include "chrome/test/in_process_browser_test.h"
+#include "googleurl/src/gurl.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+
+typedef InProcessBrowserTest WindowAppleScriptTest;
+
+// Create a window in default/normal mode.
+IN_PROC_BROWSER_TEST_F(WindowAppleScriptTest, DefaultCreation) {
+ scoped_nsobject<WindowAppleScript> aWindow(
+ [[WindowAppleScript alloc] init]);
+ EXPECT_TRUE(aWindow.get());
+ NSString* mode = [aWindow.get() mode];
+ EXPECT_NSEQ(AppleScript::kNormalWindowMode,
+ mode);
+}
+
+// Create a window with a |NULL profile|.
+IN_PROC_BROWSER_TEST_F(WindowAppleScriptTest, CreationWithNoProfile) {
+ scoped_nsobject<WindowAppleScript> aWindow(
+ [[WindowAppleScript alloc] initWithProfile:NULL]);
+ EXPECT_FALSE(aWindow.get());
+}
+
+// Create a window with a particular profile.
+IN_PROC_BROWSER_TEST_F(WindowAppleScriptTest, CreationWithProfile) {
+ Profile* defaultProfile = [[NSApp delegate] defaultProfile];
+ scoped_nsobject<WindowAppleScript> aWindow(
+ [[WindowAppleScript alloc] initWithProfile:defaultProfile]);
+ EXPECT_TRUE(aWindow.get());
+ EXPECT_TRUE([aWindow.get() uniqueID]);
+}
+
+// Create a window with no |Browser*|.
+IN_PROC_BROWSER_TEST_F(WindowAppleScriptTest, CreationWithNoBrowser) {
+ scoped_nsobject<WindowAppleScript> aWindow(
+ [[WindowAppleScript alloc] initWithBrowser:NULL]);
+ EXPECT_FALSE(aWindow.get());
+}
+
+// Create a window with |Browser*| already present.
+IN_PROC_BROWSER_TEST_F(WindowAppleScriptTest, CreationWithBrowser) {
+ scoped_nsobject<WindowAppleScript> aWindow(
+ [[WindowAppleScript alloc] initWithBrowser:browser()]);
+ EXPECT_TRUE(aWindow.get());
+ EXPECT_TRUE([aWindow.get() uniqueID]);
+}
+
+// Tabs within the window.
+IN_PROC_BROWSER_TEST_F(WindowAppleScriptTest, Tabs) {
+ scoped_nsobject<WindowAppleScript> aWindow(
+ [[WindowAppleScript alloc] initWithBrowser:browser()]);
+ NSArray* tabs = [aWindow.get() tabs];
+ EXPECT_EQ(1U, [tabs count]);
+ TabAppleScript* tab1 = [tabs objectAtIndex:0];
+ EXPECT_EQ([tab1 container], aWindow.get());
+ EXPECT_NSEQ(AppleScript::kTabsProperty,
+ [tab1 containerProperty]);
+}
+
+// Insert a new tab.
+IN_PROC_BROWSER_TEST_F(WindowAppleScriptTest, InsertTab) {
+ // Emulate what applescript would do when creating a new tab.
+ // Emulates a script like |set var to make new tab with
+ // properties URL:"http://google.com"}|.
+ scoped_nsobject<TabAppleScript> aTab([[TabAppleScript alloc] init]);
+ scoped_nsobject<NSNumber> var([[aTab.get() uniqueID] copy]);
+ [aTab.get() setURL:@"http://google.com"];
+ scoped_nsobject<WindowAppleScript> aWindow(
+ [[WindowAppleScript alloc] initWithBrowser:browser()]);
+ [aWindow.get() insertInTabs:aTab.get()];
+
+ // Represents the tab after it is inserted.
+ TabAppleScript* tab = [[aWindow.get() tabs] objectAtIndex:1];
+ EXPECT_EQ(GURL("http://google.com"),
+ GURL(base::SysNSStringToUTF8([tab URL])));
+ EXPECT_EQ([tab container], aWindow.get());
+ EXPECT_NSEQ(AppleScript::kTabsProperty,
+ [tab containerProperty]);
+ EXPECT_NSEQ(var.get(), [tab uniqueID]);
+}
+
+// Insert a new tab at a particular position
+IN_PROC_BROWSER_TEST_F(WindowAppleScriptTest, InsertTabAtPosition) {
+ // Emulate what applescript would do when creating a new tab.
+ // Emulates a script like |set var to make new tab with
+ // properties URL:"http://google.com"} at before tab 1|.
+ scoped_nsobject<TabAppleScript> aTab([[TabAppleScript alloc] init]);
+ scoped_nsobject<NSNumber> var([[aTab.get() uniqueID] copy]);
+ [aTab.get() setURL:@"http://google.com"];
+ scoped_nsobject<WindowAppleScript> aWindow(
+ [[WindowAppleScript alloc] initWithBrowser:browser()]);
+ [aWindow.get() insertInTabs:aTab.get() atIndex:0];
+
+ // Represents the tab after it is inserted.
+ TabAppleScript* tab = [[aWindow.get() tabs] objectAtIndex:0];
+ EXPECT_EQ(GURL("http://google.com"),
+ GURL(base::SysNSStringToUTF8([tab URL])));
+ EXPECT_EQ([tab container], aWindow.get());
+ EXPECT_NSEQ(AppleScript::kTabsProperty, [tab containerProperty]);
+ EXPECT_NSEQ(var.get(), [tab uniqueID]);
+}
+
+// Inserting and deleting tabs.
+IN_PROC_BROWSER_TEST_F(WindowAppleScriptTest, InsertAndDeleteTabs) {
+ scoped_nsobject<WindowAppleScript> aWindow(
+ [[WindowAppleScript alloc] initWithBrowser:browser()]);
+ scoped_nsobject<TabAppleScript> aTab;
+ int count;
+ for (int i = 0; i < 5; ++i) {
+ for (int j = 0; j < 3; ++j) {
+ aTab.reset([[TabAppleScript alloc] init]);
+ [aWindow.get() insertInTabs:aTab.get()];
+ }
+ count = 3 * i + 4;
+ EXPECT_EQ((int)[[aWindow.get() tabs] count], count);
+ }
+
+ count = (int)[[aWindow.get() tabs] count];
+ for (int i = 0; i < 5; ++i) {
+ for(int j = 0; j < 3; ++j) {
+ [aWindow.get() removeFromTabsAtIndex:0];
+ }
+ count = count - 3;
+ EXPECT_EQ((int)[[aWindow.get() tabs] count], count);
+ }
+}
+
+// Getting and setting values from the NSWindow.
+IN_PROC_BROWSER_TEST_F(WindowAppleScriptTest, NSWindowTest) {
+ scoped_nsobject<WindowAppleScript> aWindow(
+ [[WindowAppleScript alloc] initWithBrowser:browser()]);
+ [aWindow.get() setValue:[NSNumber numberWithBool:YES]
+ forKey:@"isMiniaturized"];
+ EXPECT_TRUE([[aWindow.get() valueForKey:@"isMiniaturized"] boolValue]);
+ [aWindow.get() setValue:[NSNumber numberWithBool:NO]
+ forKey:@"isMiniaturized"];
+ EXPECT_FALSE([[aWindow.get() valueForKey:@"isMiniaturized"] boolValue]);
+}
+
+// Getting and setting the active tab.
+IN_PROC_BROWSER_TEST_F(WindowAppleScriptTest, ActiveTab) {
+ scoped_nsobject<WindowAppleScript> aWindow(
+ [[WindowAppleScript alloc] initWithBrowser:browser()]);
+ scoped_nsobject<TabAppleScript> aTab([[TabAppleScript alloc] init]);
+ [aWindow.get() insertInTabs:aTab.get()];
+ [aWindow.get() setActiveTabIndex:[NSNumber numberWithInt:2]];
+ EXPECT_EQ(2, [[aWindow.get() activeTabIndex] intValue]);
+ TabAppleScript* tab2 = [[aWindow.get() tabs] objectAtIndex:1];
+ EXPECT_NSEQ([[aWindow.get() activeTab] uniqueID],
+ [tab2 uniqueID]);
+}
+
+// Order of windows.
+IN_PROC_BROWSER_TEST_F(WindowAppleScriptTest, WindowOrder) {
+ scoped_nsobject<WindowAppleScript> window2(
+ [[WindowAppleScript alloc] initWithBrowser:browser()]);
+ scoped_nsobject<WindowAppleScript> window1(
+ [[WindowAppleScript alloc] init]);
+ EXPECT_EQ([window1.get() windowComparator:window2.get()], NSOrderedAscending);
+ EXPECT_EQ([window2.get() windowComparator:window1.get()],
+ NSOrderedDescending);
+}
diff --git a/chrome/browser/ui/cocoa/authorization_util.h b/chrome/browser/ui/cocoa/authorization_util.h
new file mode 100644
index 0000000..9694998
--- /dev/null
+++ b/chrome/browser/ui/cocoa/authorization_util.h
@@ -0,0 +1,67 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_AUTHORIZATION_UTIL_H_
+#define CHROME_BROWSER_UI_COCOA_AUTHORIZATION_UTIL_H_
+#pragma once
+
+// AuthorizationExecuteWithPrivileges fork()s and exec()s the tool, but it
+// does not wait() for it. It also doesn't provide the caller with access to
+// the forked pid. If used irresponsibly, zombie processes will accumulate.
+//
+// Apple's really gotten us between a rock and a hard place, here.
+//
+// Fortunately, AuthorizationExecuteWithPrivileges does give access to the
+// tool's stdout (and stdin) via a FILE* pipe. The tool can output its pid
+// to this pipe, and the main program can read it, and then have something
+// that it can wait() for.
+//
+// The contract is that any tool executed by the wrappers declared in this
+// file must print its pid to stdout on a line by itself before doing anything
+// else.
+//
+// http://developer.apple.com/mac/library/samplecode/BetterAuthorizationSample/listing1.html
+// (Look for "What's This About Zombies?")
+
+#include <CoreFoundation/CoreFoundation.h>
+#include <Security/Authorization.h>
+#include <stdio.h>
+#include <sys/types.h>
+
+namespace authorization_util {
+
+// Obtains an AuthorizationRef that can be used to run commands as root. If
+// necessary, prompts the user for authentication. If the user is prompted,
+// |prompt| will be used as the prompt string and an icon appropriate for the
+// application will be displayed in a prompt dialog. Note that the system
+// appends its own text to the prompt string. Returns NULL on failure.
+AuthorizationRef AuthorizationCreateToRunAsRoot(CFStringRef prompt);
+
+// Calls straight through to AuthorizationExecuteWithPrivileges. If that
+// call succeeds, |pid| will be set to the pid of the executed tool. If the
+// pid can't be determined, |pid| will be set to -1. |pid| must not be NULL.
+// |pipe| may be NULL, but the tool will always be executed with a pipe in
+// order to read the pid from its stdout.
+OSStatus ExecuteWithPrivilegesAndGetPID(AuthorizationRef authorization,
+ const char* tool_path,
+ AuthorizationFlags options,
+ const char** arguments,
+ FILE** pipe,
+ pid_t* pid);
+
+// Calls ExecuteWithPrivilegesAndGetPID, and if that call succeeds, calls
+// waitpid() to wait for the process to exit. If waitpid() succeeds, the
+// exit status is placed in |exit_status|, otherwise, -1 is stored.
+// |exit_status| may be NULL and this function will still wait for the process
+// to exit.
+OSStatus ExecuteWithPrivilegesAndWait(AuthorizationRef authorization,
+ const char* tool_path,
+ AuthorizationFlags options,
+ const char** arguments,
+ FILE** pipe,
+ int* exit_status);
+
+} // namespace authorization_util
+
+#endif // CHROME_BROWSER_UI_COCOA_AUTHORIZATION_UTIL_H_
diff --git a/chrome/browser/ui/cocoa/authorization_util.mm b/chrome/browser/ui/cocoa/authorization_util.mm
new file mode 100644
index 0000000..e92dd53
--- /dev/null
+++ b/chrome/browser/ui/cocoa/authorization_util.mm
@@ -0,0 +1,184 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/authorization_util.h"
+
+#import <Foundation/Foundation.h>
+#include <sys/wait.h>
+
+#include <string>
+
+#include "base/basictypes.h"
+#include "base/eintr_wrapper.h"
+#include "base/logging.h"
+#import "base/mac_util.h"
+#include "base/string_number_conversions.h"
+#include "base/string_util.h"
+#include "chrome/browser/ui/cocoa/scoped_authorizationref.h"
+
+namespace authorization_util {
+
+AuthorizationRef AuthorizationCreateToRunAsRoot(CFStringRef prompt) {
+ // Create an empty AuthorizationRef.
+ scoped_AuthorizationRef authorization;
+ OSStatus status = AuthorizationCreate(NULL,
+ kAuthorizationEmptyEnvironment,
+ kAuthorizationFlagDefaults,
+ &authorization);
+ if (status != errAuthorizationSuccess) {
+ LOG(ERROR) << "AuthorizationCreate: " << status;
+ return NULL;
+ }
+
+ // Specify the "system.privilege.admin" right, which allows
+ // AuthorizationExecuteWithPrivileges to run commands as root.
+ AuthorizationItem right_items[] = {
+ {kAuthorizationRightExecute, 0, NULL, 0}
+ };
+ AuthorizationRights rights = {arraysize(right_items), right_items};
+
+ // product_logo_32.png is used instead of app.icns because Authorization
+ // Services can't deal with .icns files.
+ NSString* icon_path =
+ [mac_util::MainAppBundle() pathForResource:@"product_logo_32"
+ ofType:@"png"];
+ const char* icon_path_c = [icon_path fileSystemRepresentation];
+ size_t icon_path_length = icon_path_c ? strlen(icon_path_c) : 0;
+
+ // The OS will append " Type an administrator's name and password to allow
+ // <CFBundleDisplayName> to make changes."
+ NSString* prompt_ns = const_cast<NSString*>(
+ reinterpret_cast<const NSString*>(prompt));
+ const char* prompt_c = [prompt_ns UTF8String];
+ size_t prompt_length = prompt_c ? strlen(prompt_c) : 0;
+
+ AuthorizationItem environment_items[] = {
+ {kAuthorizationEnvironmentIcon, icon_path_length, (void*)icon_path_c, 0},
+ {kAuthorizationEnvironmentPrompt, prompt_length, (void*)prompt_c, 0}
+ };
+
+ AuthorizationEnvironment environment = {arraysize(environment_items),
+ environment_items};
+
+ AuthorizationFlags flags = kAuthorizationFlagDefaults |
+ kAuthorizationFlagInteractionAllowed |
+ kAuthorizationFlagExtendRights |
+ kAuthorizationFlagPreAuthorize;
+
+ status = AuthorizationCopyRights(authorization,
+ &rights,
+ &environment,
+ flags,
+ NULL);
+ if (status != errAuthorizationSuccess) {
+ if (status != errAuthorizationCanceled) {
+ LOG(ERROR) << "AuthorizationCopyRights: " << status;
+ }
+ return NULL;
+ }
+
+ return authorization.release();
+}
+
+OSStatus ExecuteWithPrivilegesAndGetPID(AuthorizationRef authorization,
+ const char* tool_path,
+ AuthorizationFlags options,
+ const char** arguments,
+ FILE** pipe,
+ pid_t* pid) {
+ // pipe may be NULL, but this function needs one. In that case, use a local
+ // pipe.
+ FILE* local_pipe;
+ FILE** pipe_pointer;
+ if (pipe) {
+ pipe_pointer = pipe;
+ } else {
+ pipe_pointer = &local_pipe;
+ }
+
+ // AuthorizationExecuteWithPrivileges wants |char* const*| for |arguments|,
+ // but it doesn't actually modify the arguments, and that type is kind of
+ // silly and callers probably aren't dealing with that. Put the cast here
+ // to make things a little easier on callers.
+ OSStatus status = AuthorizationExecuteWithPrivileges(authorization,
+ tool_path,
+ options,
+ (char* const*)arguments,
+ pipe_pointer);
+ if (status != errAuthorizationSuccess) {
+ return status;
+ }
+
+ int line_pid = -1;
+ size_t line_length = 0;
+ char* line_c = fgetln(*pipe_pointer, &line_length);
+ if (line_c) {
+ if (line_length > 0 && line_c[line_length - 1] == '\n') {
+ // line_c + line_length is the start of the next line if there is one.
+ // Back up one character.
+ --line_length;
+ }
+ std::string line(line_c, line_length);
+ if (!base::StringToInt(line, &line_pid)) {
+ // StringToInt may have set line_pid to something, but if the conversion
+ // was imperfect, use -1.
+ LOG(ERROR) << "ExecuteWithPrivilegesAndGetPid: funny line: " << line;
+ line_pid = -1;
+ }
+ } else {
+ LOG(ERROR) << "ExecuteWithPrivilegesAndGetPid: no line";
+ }
+
+ if (!pipe) {
+ fclose(*pipe_pointer);
+ }
+
+ if (pid) {
+ *pid = line_pid;
+ }
+
+ return status;
+}
+
+OSStatus ExecuteWithPrivilegesAndWait(AuthorizationRef authorization,
+ const char* tool_path,
+ AuthorizationFlags options,
+ const char** arguments,
+ FILE** pipe,
+ int* exit_status) {
+ pid_t pid;
+ OSStatus status = ExecuteWithPrivilegesAndGetPID(authorization,
+ tool_path,
+ options,
+ arguments,
+ pipe,
+ &pid);
+ if (status != errAuthorizationSuccess) {
+ return status;
+ }
+
+ // exit_status may be NULL, but this function needs it. In that case, use a
+ // local version.
+ int local_exit_status;
+ int* exit_status_pointer;
+ if (exit_status) {
+ exit_status_pointer = exit_status;
+ } else {
+ exit_status_pointer = &local_exit_status;
+ }
+
+ if (pid != -1) {
+ pid_t wait_result = HANDLE_EINTR(waitpid(pid, exit_status_pointer, 0));
+ if (wait_result != pid) {
+ PLOG(ERROR) << "waitpid";
+ *exit_status_pointer = -1;
+ }
+ } else {
+ *exit_status_pointer = -1;
+ }
+
+ return status;
+}
+
+} // namespace authorization_util
diff --git a/chrome/browser/ui/cocoa/back_forward_menu_controller.h b/chrome/browser/ui/cocoa/back_forward_menu_controller.h
new file mode 100644
index 0000000..6ec82f6
--- /dev/null
+++ b/chrome/browser/ui/cocoa/back_forward_menu_controller.h
@@ -0,0 +1,43 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_BACK_FORWARD_MENU_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_BACK_FORWARD_MENU_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/back_forward_menu_model.h"
+
+@class DelayedMenuButton;
+
+typedef BackForwardMenuModel::ModelType BackForwardMenuType;
+const BackForwardMenuType BACK_FORWARD_MENU_TYPE_BACK =
+ BackForwardMenuModel::BACKWARD_MENU;
+const BackForwardMenuType BACK_FORWARD_MENU_TYPE_FORWARD =
+ BackForwardMenuModel::FORWARD_MENU;
+
+// A class that manages the back/forward menu (and delayed-menu button, and
+// model).
+
+@interface BackForwardMenuController : NSObject {
+ @private
+ BackForwardMenuType type_;
+ DelayedMenuButton* button_; // Weak; comes from nib.
+ scoped_ptr<BackForwardMenuModel> model_;
+ scoped_nsobject<NSMenu> backForwardMenu_;
+}
+
+// Type (back or forwards); can only be set on initialization.
+@property(readonly, nonatomic) BackForwardMenuType type;
+
+- (id)initWithBrowser:(Browser*)browser
+ modelType:(BackForwardMenuType)type
+ button:(DelayedMenuButton*)button;
+
+@end // @interface BackForwardMenuController
+
+#endif // CHROME_BROWSER_UI_COCOA_BACK_FORWARD_MENU_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/back_forward_menu_controller.mm b/chrome/browser/ui/cocoa/back_forward_menu_controller.mm
new file mode 100644
index 0000000..a3e89b2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/back_forward_menu_controller.mm
@@ -0,0 +1,102 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/back_forward_menu_controller.h"
+
+#include "base/logging.h"
+#include "base/scoped_ptr.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/back_forward_menu_model.h"
+#import "chrome/browser/ui/cocoa/delayedmenu_button.h"
+#import "chrome/browser/ui/cocoa/event_utils.h"
+#include "skia/ext/skia_utils_mac.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+using base::SysUTF16ToNSString;
+using gfx::SkBitmapToNSImage;
+
+@implementation BackForwardMenuController
+
+// Accessors and mutators:
+
+@synthesize type = type_;
+
+// Own methods:
+
+- (id)initWithBrowser:(Browser*)browser
+ modelType:(BackForwardMenuType)type
+ button:(DelayedMenuButton*)button {
+ if ((self = [super init])) {
+ type_ = type;
+ button_ = button;
+ model_.reset(new BackForwardMenuModel(browser, type_));
+ DCHECK(model_.get());
+ backForwardMenu_.reset([[NSMenu alloc] initWithTitle:@""]);
+ DCHECK(backForwardMenu_.get());
+ [backForwardMenu_ setDelegate:self];
+
+ [button_ setAttachedMenu:backForwardMenu_];
+ [button_ setAttachedMenuEnabled:YES];
+ }
+ return self;
+}
+
+// Methods as delegate:
+
+// Called by backForwardMenu_ just before tracking begins.
+//TODO(viettrungluu): should we do anything for chapter stops (see model)?
+- (void)menuNeedsUpdate:(NSMenu*)menu {
+ DCHECK(menu == backForwardMenu_);
+
+ // Remove old menu items (backwards order is as good as any).
+ for (NSInteger i = [menu numberOfItems]; i > 0; i--)
+ [menu removeItemAtIndex:(i - 1)];
+
+ // 0-th item must be blank. (This is because we use a pulldown list, for which
+ // Cocoa uses the 0-th item as "title" in the button.)
+ [menu insertItemWithTitle:@""
+ action:nil
+ keyEquivalent:@""
+ atIndex:0];
+ for (int menuID = 0; menuID < model_->GetItemCount(); menuID++) {
+ if (model_->IsSeparator(menuID)) {
+ [menu insertItem:[NSMenuItem separatorItem]
+ atIndex:(menuID + 1)];
+ } else {
+ // Create a menu item with the right label.
+ NSMenuItem* menuItem = [[NSMenuItem alloc]
+ initWithTitle:SysUTF16ToNSString(model_->GetLabelAt(menuID))
+ action:nil
+ keyEquivalent:@""];
+ [menuItem autorelease];
+
+ SkBitmap icon;
+ // Icon (if it has one).
+ if (model_->GetIconAt(menuID, &icon))
+ [menuItem setImage:SkBitmapToNSImage(icon)];
+
+ // This will make it call our |-executeMenuItem:| method. We store the
+ // |menuID| (or |menu_id|) in the tag.
+ [menuItem setTag:menuID];
+ [menuItem setTarget:self];
+ [menuItem setAction:@selector(executeMenuItem:)];
+
+ // Put it in the menu!
+ [menu insertItem:menuItem
+ atIndex:(menuID + 1)];
+ }
+ }
+}
+
+// Action methods:
+
+- (void)executeMenuItem:(id)sender {
+ DCHECK([sender isKindOfClass:[NSMenuItem class]]);
+ int menuID = [sender tag];
+ model_->ActivatedAtWithDisposition(
+ menuID,
+ event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]));
+}
+
+@end // @implementation BackForwardMenuController
diff --git a/chrome/browser/ui/cocoa/background_gradient_view.h b/chrome/browser/ui/cocoa/background_gradient_view.h
new file mode 100644
index 0000000..d72fa57
--- /dev/null
+++ b/chrome/browser/ui/cocoa/background_gradient_view.h
@@ -0,0 +1,29 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_BACKGROUND_GRADIENT_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_BACKGROUND_GRADIENT_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// A custom view that draws a 'standard' background gradient.
+// Base class for other Chromium views.
+@interface BackgroundGradientView : NSView {
+ @private
+ BOOL showsDivider_;
+}
+
+// The color used for the bottom stroke. Public so subclasses can use.
+- (NSColor *)strokeColor;
+
+// Draws the background for this view. Make sure that your patternphase
+// is set up correctly in your graphics context before calling.
+- (void)drawBackground;
+
+// Controls whether the bar draws a dividing line at the bottom.
+@property(nonatomic, assign) BOOL showsDivider;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_BACKGROUND_GRADIENT_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/background_gradient_view.mm b/chrome/browser/ui/cocoa/background_gradient_view.mm
new file mode 100644
index 0000000..1c5735f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/background_gradient_view.mm
@@ -0,0 +1,81 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/background_gradient_view.h"
+
+#import "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#include "grit/theme_resources.h"
+
+#define kToolbarTopOffset 12
+#define kToolbarMaxHeight 100
+
+@implementation BackgroundGradientView
+@synthesize showsDivider = showsDivider_;
+
+- (id)initWithFrame:(NSRect)frameRect {
+ self = [super initWithFrame:frameRect];
+ if (self != nil) {
+ showsDivider_ = YES;
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ showsDivider_ = YES;
+}
+
+- (void)setShowsDivider:(BOOL)show {
+ showsDivider_ = show;
+ [self setNeedsDisplay:YES];
+}
+
+- (void)drawBackground {
+ BOOL isKey = [[self window] isKeyWindow];
+ ThemeProvider* themeProvider = [[self window] themeProvider];
+ if (themeProvider) {
+ NSColor* backgroundImageColor =
+ themeProvider->GetNSImageColorNamed(IDR_THEME_TOOLBAR, false);
+ if (backgroundImageColor) {
+ [backgroundImageColor set];
+ NSRectFill([self bounds]);
+ } else {
+ CGFloat winHeight = NSHeight([[self window] frame]);
+ NSGradient* gradient = themeProvider->GetNSGradient(
+ isKey ? BrowserThemeProvider::GRADIENT_TOOLBAR :
+ BrowserThemeProvider::GRADIENT_TOOLBAR_INACTIVE);
+ NSPoint startPoint =
+ [self convertPoint:NSMakePoint(0, winHeight - kToolbarTopOffset)
+ fromView:nil];
+ NSPoint endPoint =
+ NSMakePoint(0, winHeight - kToolbarTopOffset - kToolbarMaxHeight);
+ endPoint = [self convertPoint:endPoint fromView:nil];
+
+ [gradient drawFromPoint:startPoint
+ toPoint:endPoint
+ options:(NSGradientDrawsBeforeStartingLocation |
+ NSGradientDrawsAfterEndingLocation)];
+ }
+
+ if (showsDivider_) {
+ // Draw bottom stroke
+ [[self strokeColor] set];
+ NSRect borderRect, contentRect;
+ NSDivideRect([self bounds], &borderRect, &contentRect, 1, NSMinYEdge);
+ NSRectFillUsingOperation(borderRect, NSCompositeSourceOver);
+ }
+ }
+}
+
+- (NSColor*)strokeColor {
+ BOOL isKey = [[self window] isKeyWindow];
+ ThemeProvider* themeProvider = [[self window] themeProvider];
+ if (!themeProvider)
+ return [NSColor blackColor];
+ return themeProvider->GetNSColor(
+ isKey ? BrowserThemeProvider::COLOR_TOOLBAR_STROKE :
+ BrowserThemeProvider::COLOR_TOOLBAR_STROKE_INACTIVE, true);
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/background_gradient_view_unittest.mm b/chrome/browser/ui/cocoa/background_gradient_view_unittest.mm
new file mode 100644
index 0000000..693c21a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/background_gradient_view_unittest.mm
@@ -0,0 +1,47 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/background_gradient_view.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+// Since BackgroundGradientView doesn't do any drawing by default, we
+// create a subclass to call its draw method for us.
+@interface BackgroundGradientSubClassTest : BackgroundGradientView
+@end
+
+@implementation BackgroundGradientSubClassTest
+- (void)drawRect:(NSRect)rect {
+ [self drawBackground];
+}
+@end
+
+namespace {
+
+class BackgroundGradientViewTest : public CocoaTest {
+ public:
+ BackgroundGradientViewTest() {
+ NSRect frame = NSMakeRect(0, 0, 100, 30);
+ scoped_nsobject<BackgroundGradientSubClassTest> view;
+ view.reset([[BackgroundGradientSubClassTest alloc] initWithFrame:frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ BackgroundGradientSubClassTest* view_;
+};
+
+TEST_VIEW(BackgroundGradientViewTest, view_)
+
+// Test drawing, mostly to ensure nothing leaks or crashes.
+TEST_F(BackgroundGradientViewTest, DisplayWithDivider) {
+ [view_ setShowsDivider:YES];
+ [view_ display];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/background_tile_view.h b/chrome/browser/ui/cocoa/background_tile_view.h
new file mode 100644
index 0000000..9a08113
--- /dev/null
+++ b/chrome/browser/ui/cocoa/background_tile_view.h
@@ -0,0 +1,23 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_BACKGROUND_TILE_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_BACKGROUND_TILE_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// A custom view that draws a image tiled as the background. This isn't meant
+// to be used where themes might be need, and is for other windows (about box).
+
+@interface BackgroundTileView : NSView {
+ @private
+ BOOL showsDivider_;
+ NSImage* tileImage_;
+}
+
+@property(nonatomic, retain) NSImage* tileImage;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_BACKGROUND_TILE_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/background_tile_view.mm b/chrome/browser/ui/cocoa/background_tile_view.mm
new file mode 100644
index 0000000..e63141b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/background_tile_view.mm
@@ -0,0 +1,32 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/background_tile_view.h"
+
+@implementation BackgroundTileView
+@synthesize tileImage = tileImage_;
+
+- (void)setTileImage:(NSImage*)tileImage {
+ [tileImage_ autorelease];
+ tileImage_ = [tileImage retain];
+ [self setNeedsDisplay:YES];
+}
+
+- (void)drawRect:(NSRect)rect {
+ // Tile within the view, so set the phase to start at the view bottom.
+ NSPoint phase = NSMakePoint(0.0, NSMinY([self frame]));
+ [[NSGraphicsContext currentContext] setPatternPhase:phase];
+
+ if (tileImage_) {
+ NSColor *color = [NSColor colorWithPatternImage:tileImage_];
+ [color set];
+ } else {
+ // Something to catch the missing image
+ [[NSColor magentaColor] set];
+ }
+
+ NSRectFill([self bounds]);
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/background_tile_view_unittest.mm b/chrome/browser/ui/cocoa/background_tile_view_unittest.mm
new file mode 100644
index 0000000..4af5751
--- /dev/null
+++ b/chrome/browser/ui/cocoa/background_tile_view_unittest.mm
@@ -0,0 +1,37 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/background_tile_view.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class BackgroundTileViewTest : public CocoaTest {
+ public:
+ BackgroundTileViewTest() {
+ NSRect frame = NSMakeRect(0, 0, 100, 30);
+ scoped_nsobject<BackgroundTileView> view([[BackgroundTileView alloc]
+ initWithFrame:frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ BackgroundTileView *view_;
+};
+
+TEST_VIEW(BackgroundTileViewTest, view_)
+
+// Test drawing with an Image
+TEST_F(BackgroundTileViewTest, DisplayImage) {
+ NSImage* image = [NSImage imageNamed:@"NSApplicationIcon"];
+ [view_ setTileImage:image];
+ [view_ display];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/base_bubble_controller.h b/chrome/browser/ui/cocoa/base_bubble_controller.h
new file mode 100644
index 0000000..7cc2c5b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/base_bubble_controller.h
@@ -0,0 +1,67 @@
+// 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 "base/cocoa_protocols_mac.h"
+#include "base/scoped_ptr.h"
+
+namespace BaseBubbleControllerInternal {
+class Bridge;
+}
+
+@class InfoBubbleView;
+
+// Base class for bubble controllers. Manages a xib that contains an
+// InfoBubbleWindow which contains an InfoBubbleView. Contains code to close
+// the bubble window on clicks outside of the window, and the like.
+// To use this class:
+// 1. Create a new xib that contains a window. Change the window's class to
+// InfoBubbleWindow. Give it a child view that autosizes to the window's full
+// size, give it class InfoBubbleView. Make the controller the window's
+// delegate.
+// 2. Create a subclass of BaseBubbleController.
+// 3. Change the xib's File Owner to your subclass.
+// 4. Hook up the File Owner's |bubble_| to the InfoBubbleView in the xib.
+@interface BaseBubbleController : NSWindowController<NSWindowDelegate> {
+ @private
+ NSWindow* parentWindow_; // weak
+ NSPoint anchor_;
+ IBOutlet InfoBubbleView* bubble_; // to set arrow position
+ // Bridge that listens for notifications.
+ scoped_ptr<BaseBubbleControllerInternal::Bridge> base_bridge_;
+}
+
+@property (nonatomic, readonly) NSWindow* parentWindow;
+@property (nonatomic, assign) NSPoint anchorPoint;
+@property (nonatomic, readonly) InfoBubbleView* bubble;
+
+// Creates a bubble. |nibPath| is just the basename, e.g. @"FirstRunBubble".
+// |anchoredAt| is in screen space. You need to call -showWindow: to make the
+// bubble visible. It will autorelease itself when the user dismisses the
+// bubble.
+// This is the designated initializer.
+- (id)initWithWindowNibPath:(NSString*)nibPath
+ parentWindow:(NSWindow*)parentWindow
+ anchoredAt:(NSPoint)anchoredAt;
+
+
+// Creates a bubble. |nibPath| is just the basename, e.g. @"FirstRunBubble".
+// |view| must be in a window. The bubble will point at |offset| relative to
+// |view|'s lower left corner. You need to call -showWindow: to make the
+// bubble visible. It will autorelease itself when the user dismisses the
+// bubble.
+- (id)initWithWindowNibPath:(NSString*)nibPath
+ relativeToView:(NSView*)view
+ offset:(NSPoint)offset;
+
+
+// For subclasses that do not load from a XIB, this will simply set the instance
+// variables appropriately. This will also replace the |-[self window]|'s
+// contentView with an instance of InfoBubbleView.
+- (id)initWithWindow:(NSWindow*)theWindow
+ parentWindow:(NSWindow*)parentWindow
+ anchoredAt:(NSPoint)anchoredAt;
+
+@end
diff --git a/chrome/browser/ui/cocoa/base_bubble_controller.mm b/chrome/browser/ui/cocoa/base_bubble_controller.mm
new file mode 100644
index 0000000..1ebef4c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/base_bubble_controller.mm
@@ -0,0 +1,201 @@
+// 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/ui/cocoa/base_bubble_controller.h"
+
+#include "app/l10n_util.h"
+#include "base/logging.h"
+#include "base/mac_util.h"
+#include "base/scoped_nsobject.h"
+#include "base/string_util.h"
+#import "chrome/browser/ui/cocoa/info_bubble_view.h"
+#include "chrome/common/notification_observer.h"
+#include "chrome/common/notification_registrar.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/notification_type.h"
+#include "grit/generated_resources.h"
+
+@interface BaseBubbleController (Private)
+- (void)updateOriginFromAnchor;
+@end
+
+namespace BaseBubbleControllerInternal {
+
+// This bridge listens for notifications so that the bubble closes when a user
+// switches tabs (including by opening a new one).
+class Bridge : public NotificationObserver {
+ public:
+ explicit Bridge(BaseBubbleController* controller) : controller_(controller) {
+ registrar_.Add(this, NotificationType::TAB_CONTENTS_HIDDEN,
+ NotificationService::AllSources());
+ }
+
+ // NotificationObserver:
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ [controller_ close];
+ }
+
+ private:
+ BaseBubbleController* controller_; // Weak, owns this.
+ NotificationRegistrar registrar_;
+};
+
+} // namespace BaseBubbleControllerInternal
+
+@implementation BaseBubbleController
+
+@synthesize parentWindow = parentWindow_;
+@synthesize anchorPoint = anchor_;
+@synthesize bubble = bubble_;
+
+- (id)initWithWindowNibPath:(NSString*)nibPath
+ parentWindow:(NSWindow*)parentWindow
+ anchoredAt:(NSPoint)anchoredAt {
+ nibPath = [mac_util::MainAppBundle() pathForResource:nibPath
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
+ parentWindow_ = parentWindow;
+ anchor_ = anchoredAt;
+
+ // Watch to see if the parent window closes, and if so, close this one.
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
+ selector:@selector(parentWindowWillClose:)
+ name:NSWindowWillCloseNotification
+ object:parentWindow_];
+ }
+ return self;
+}
+
+- (id)initWithWindowNibPath:(NSString*)nibPath
+ relativeToView:(NSView*)view
+ offset:(NSPoint)offset {
+ DCHECK([view window]);
+ NSWindow* window = [view window];
+ NSRect bounds = [view convertRect:[view bounds] toView:nil];
+ NSPoint anchor = NSMakePoint(NSMinX(bounds) + offset.x,
+ NSMinY(bounds) + offset.y);
+ anchor = [window convertBaseToScreen:anchor];
+ return [self initWithWindowNibPath:nibPath
+ parentWindow:window
+ anchoredAt:anchor];
+}
+
+- (id)initWithWindow:(NSWindow*)theWindow
+ parentWindow:(NSWindow*)parentWindow
+ anchoredAt:(NSPoint)anchoredAt {
+ DCHECK(theWindow);
+ if ((self = [super initWithWindow:theWindow])) {
+ parentWindow_ = parentWindow;
+ anchor_ = anchoredAt;
+
+ DCHECK(![[self window] delegate]);
+ [theWindow setDelegate:self];
+
+ scoped_nsobject<InfoBubbleView> contentView(
+ [[InfoBubbleView alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)]);
+ [theWindow setContentView:contentView.get()];
+ bubble_ = contentView.get();
+
+ // Watch to see if the parent window closes, and if so, close this one.
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
+ selector:@selector(parentWindowWillClose:)
+ name:NSWindowWillCloseNotification
+ object:parentWindow_];
+
+ [self awakeFromNib];
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ // Check all connections have been made in Interface Builder.
+ DCHECK([self window]);
+ DCHECK(bubble_);
+ DCHECK_EQ(self, [[self window] delegate]);
+
+ base_bridge_.reset(new BaseBubbleControllerInternal::Bridge(self));
+
+ [bubble_ setBubbleType:info_bubble::kWhiteInfoBubble];
+ [bubble_ setArrowLocation:info_bubble::kTopRight];
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+- (void)setAnchorPoint:(NSPoint)anchor {
+ anchor_ = anchor;
+ [self updateOriginFromAnchor];
+}
+
+- (void)parentWindowWillClose:(NSNotification*)notification {
+ [self close];
+}
+
+- (void)windowWillClose:(NSNotification*)notification {
+ // We caught a close so we don't need to watch for the parent closing.
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [self autorelease];
+}
+
+// We want this to be a child of a browser window. addChildWindow:
+// (called from this function) will bring the window on-screen;
+// unfortunately, [NSWindowController showWindow:] will also bring it
+// on-screen (but will cause unexpected changes to the window's
+// position). We cannot have an addChildWindow: and a subsequent
+// showWindow:. Thus, we have our own version.
+- (void)showWindow:(id)sender {
+ NSWindow* window = [self window]; // completes nib load
+ [self updateOriginFromAnchor];
+ [parentWindow_ addChildWindow:window ordered:NSWindowAbove];
+ [window makeKeyAndOrderFront:self];
+}
+
+- (void)close {
+ [parentWindow_ removeChildWindow:[self window]];
+ [super close];
+}
+
+// The controller is the delegate of the window so it receives did resign key
+// notifications. When key is resigned mirror Windows behavior and close the
+// window.
+- (void)windowDidResignKey:(NSNotification*)notification {
+ NSWindow* window = [self window];
+ DCHECK_EQ([notification object], window);
+ if ([window isVisible]) {
+ // If the window isn't visible, it is already closed, and this notification
+ // has been sent as part of the closing operation, so no need to close.
+ [self close];
+ }
+}
+
+// By implementing this, ESC causes the window to go away.
+- (IBAction)cancel:(id)sender {
+ // This is not a "real" cancel as potential changes to the radio group are not
+ // undone. That's ok.
+ [self close];
+}
+
+// Takes the |anchor_| point and adjusts the window's origin accordingly.
+- (void)updateOriginFromAnchor {
+ NSWindow* window = [self window];
+ NSPoint origin = anchor_;
+ NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
+ info_bubble::kBubbleArrowWidth / 2.0, 0);
+ offsets = [[parentWindow_ contentView] convertSize:offsets toView:nil];
+ if ([bubble_ arrowLocation] == info_bubble::kTopRight) {
+ origin.x -= NSWidth([window frame]) - offsets.width;
+ } else {
+ origin.x -= offsets.width;
+ }
+ origin.y -= NSHeight([window frame]);
+ [window setFrameOrigin:origin];
+}
+
+@end // BaseBubbleController
diff --git a/chrome/browser/ui/cocoa/base_view.h b/chrome/browser/ui/cocoa/base_view.h
new file mode 100644
index 0000000..0a8da9e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/base_view.h
@@ -0,0 +1,45 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_BASE_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_BASE_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "gfx/rect.h"
+
+// A view that provides common functionality that many views will need:
+// - Automatic registration for mouse-moved events.
+// - Funneling of mouse and key events to two methods
+// - Coordinate conversion utilities
+
+@interface BaseView : NSView {
+ @private
+ NSTrackingArea *trackingArea_;
+ BOOL dragging_;
+ scoped_nsobject<NSEvent> pendingExitEvent_;
+}
+
+- (id)initWithFrame:(NSRect)frame;
+
+// Override these methods in a subclass.
+- (void)mouseEvent:(NSEvent *)theEvent;
+- (void)keyEvent:(NSEvent *)theEvent;
+
+// Useful rect conversions (doing coordinate flipping)
+- (gfx::Rect)flipNSRectToRect:(NSRect)rect;
+- (NSRect)flipRectToNSRect:(gfx::Rect)rect;
+
+@end
+
+// A notification that a view may issue when it receives first responder status.
+// The name is |kViewDidBecomeFirstResponder|, the object is the view, and the
+// NSSelectionDirection is wrapped in an NSNumber under the key
+// |kSelectionDirection|.
+extern NSString* kViewDidBecomeFirstResponder;
+extern NSString* kSelectionDirection;
+
+#endif // CHROME_BROWSER_UI_COCOA_BASE_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/base_view.mm b/chrome/browser/ui/cocoa/base_view.mm
new file mode 100644
index 0000000..b26c390
--- /dev/null
+++ b/chrome/browser/ui/cocoa/base_view.mm
@@ -0,0 +1,147 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/base_view.h"
+
+NSString* kViewDidBecomeFirstResponder =
+ @"Chromium.kViewDidBecomeFirstResponder";
+NSString* kSelectionDirection = @"Chromium.kSelectionDirection";
+
+@implementation BaseView
+
+- (id)initWithFrame:(NSRect)frame {
+ self = [super initWithFrame:frame];
+ if (self) {
+ trackingArea_ =
+ [[NSTrackingArea alloc] initWithRect:frame
+ options:NSTrackingMouseMoved |
+ NSTrackingMouseEnteredAndExited |
+ NSTrackingActiveInActiveApp |
+ NSTrackingInVisibleRect
+ owner:self
+ userInfo:nil];
+ [self addTrackingArea:trackingArea_];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [self removeTrackingArea:trackingArea_];
+ [trackingArea_ release];
+
+ [super dealloc];
+}
+
+- (void)mouseEvent:(NSEvent *)theEvent {
+ // This method left intentionally blank.
+}
+
+- (void)keyEvent:(NSEvent *)theEvent {
+ // This method left intentionally blank.
+}
+
+- (void)mouseDown:(NSEvent *)theEvent {
+ dragging_ = YES;
+ [self mouseEvent:theEvent];
+}
+
+- (void)rightMouseDown:(NSEvent *)theEvent {
+ [self mouseEvent:theEvent];
+}
+
+- (void)otherMouseDown:(NSEvent *)theEvent {
+ [self mouseEvent:theEvent];
+}
+
+- (void)mouseUp:(NSEvent *)theEvent {
+ [self mouseEvent:theEvent];
+
+ dragging_ = NO;
+ if (pendingExitEvent_.get()) {
+ NSEvent* exitEvent =
+ [NSEvent enterExitEventWithType:NSMouseExited
+ location:[theEvent locationInWindow]
+ modifierFlags:[theEvent modifierFlags]
+ timestamp:[theEvent timestamp]
+ windowNumber:[theEvent windowNumber]
+ context:[theEvent context]
+ eventNumber:[pendingExitEvent_.get() eventNumber]
+ trackingNumber:[pendingExitEvent_.get() trackingNumber]
+ userData:[pendingExitEvent_.get() userData]];
+ [self mouseEvent:exitEvent];
+ pendingExitEvent_.reset();
+ }
+}
+
+- (void)rightMouseUp:(NSEvent *)theEvent {
+ [self mouseEvent:theEvent];
+}
+
+- (void)otherMouseUp:(NSEvent *)theEvent {
+ [self mouseEvent:theEvent];
+}
+
+- (void)mouseMoved:(NSEvent *)theEvent {
+ [self mouseEvent:theEvent];
+}
+
+- (void)mouseDragged:(NSEvent *)theEvent {
+ [self mouseEvent:theEvent];
+}
+
+- (void)rightMouseDragged:(NSEvent *)theEvent {
+ [self mouseEvent:theEvent];
+}
+
+- (void)otherMouseDragged:(NSEvent *)theEvent {
+ [self mouseEvent:theEvent];
+}
+
+- (void)mouseEntered:(NSEvent *)theEvent {
+ if (pendingExitEvent_.get()) {
+ pendingExitEvent_.reset();
+ return;
+ }
+
+ [self mouseEvent:theEvent];
+}
+
+- (void)mouseExited:(NSEvent *)theEvent {
+ // The tracking area will send an exit event even during a drag, which isn't
+ // how the event flow for drags should work. This stores the exit event, and
+ // sends it when the drag completes instead.
+ if (dragging_) {
+ pendingExitEvent_.reset([theEvent retain]);
+ return;
+ }
+
+ [self mouseEvent:theEvent];
+}
+
+- (void)keyDown:(NSEvent *)theEvent {
+ [self keyEvent:theEvent];
+}
+
+- (void)keyUp:(NSEvent *)theEvent {
+ [self keyEvent:theEvent];
+}
+
+- (void)flagsChanged:(NSEvent *)theEvent {
+ [self keyEvent:theEvent];
+}
+
+- (gfx::Rect)flipNSRectToRect:(NSRect)rect {
+ gfx::Rect new_rect(NSRectToCGRect(rect));
+ new_rect.set_y([self bounds].size.height - new_rect.y() - new_rect.height());
+ return new_rect;
+}
+
+- (NSRect)flipRectToNSRect:(gfx::Rect)rect {
+ NSRect new_rect(NSRectFromCGRect(rect.ToCGRect()));
+ new_rect.origin.y =
+ [self bounds].size.height - new_rect.origin.y - new_rect.size.height;
+ return new_rect;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/base_view_unittest.mm b/chrome/browser/ui/cocoa/base_view_unittest.mm
new file mode 100644
index 0000000..bd356b4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/base_view_unittest.mm
@@ -0,0 +1,48 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/base_view.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class BaseViewTest : public CocoaTest {
+ public:
+ BaseViewTest() {
+ NSRect frame = NSMakeRect(0, 0, 100, 100);
+ scoped_nsobject<BaseView> view([[BaseView alloc] initWithFrame:frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ BaseView* view_; // weak
+};
+
+TEST_VIEW(BaseViewTest, view_)
+
+// Convert a rect in |view_|'s Cocoa coordinate system to gfx::Rect's top-left
+// coordinate system. Repeat the process in reverse and make sure we come out
+// with the original rect.
+TEST_F(BaseViewTest, flipNSRectToRect) {
+ NSRect convert = NSMakeRect(10, 10, 50, 50);
+ gfx::Rect converted = [view_ flipNSRectToRect:convert];
+ EXPECT_EQ(converted.x(), 10);
+ EXPECT_EQ(converted.y(), 40); // Due to view being 100px tall.
+ EXPECT_EQ(converted.width(), convert.size.width);
+ EXPECT_EQ(converted.height(), convert.size.height);
+
+ // Go back the other way.
+ NSRect back_again = [view_ flipRectToNSRect:converted];
+ EXPECT_EQ(back_again.origin.x, convert.origin.x);
+ EXPECT_EQ(back_again.origin.y, convert.origin.y);
+ EXPECT_EQ(back_again.size.width, convert.size.width);
+ EXPECT_EQ(back_again.size.height, convert.size.height);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h
new file mode 100644
index 0000000..505211c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h
@@ -0,0 +1,46 @@
+// 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_UI_COCOA_BOOKMARKS_BOOKMARK_ALL_TABS_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_ALL_TABS_CONTROLLER_H_
+#pragma once
+
+#include <utility>
+#include <vector>
+
+#include "base/string16.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h"
+
+// A list of pairs containing the name and URL associated with each
+// currently active tab in the active browser window.
+typedef std::pair<string16, GURL> ActiveTabNameURLPair;
+typedef std::vector<ActiveTabNameURLPair> ActiveTabsNameURLPairVector;
+
+// A controller for the Bookmark All Tabs sheet which is presented upon
+// selecting the Bookmark All Tabs... menu item shown by the contextual
+// menu in the bookmarks bar.
+@interface BookmarkAllTabsController : BookmarkEditorBaseController {
+ @private
+ ActiveTabsNameURLPairVector activeTabPairsVector_;
+}
+
+- (id)initWithParentWindow:(NSWindow*)parentWindow
+ profile:(Profile*)profile
+ parent:(const BookmarkNode*)parent
+ configuration:(BookmarkEditor::Configuration)configuration;
+
+@end
+
+@interface BookmarkAllTabsController(TestingAPI)
+
+// Initializes the list of all tab names and URLs. Overridden by unit test
+// to provide canned test data.
+- (void)UpdateActiveTabPairs;
+
+// Provides testing access to tab pairs list.
+- (ActiveTabsNameURLPairVector*)activeTabPairsVector;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_ALL_TABS_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.mm
new file mode 100644
index 0000000..e8ebaec
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.mm
@@ -0,0 +1,88 @@
+// 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/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h"
+
+#include "app/l10n_util_mac.h"
+#include "base/string16.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents_wrapper.h"
+#include "chrome/browser/tabs/tab_strip_model.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "grit/generated_resources.h"
+
+@implementation BookmarkAllTabsController
+
+- (id)initWithParentWindow:(NSWindow*)parentWindow
+ profile:(Profile*)profile
+ parent:(const BookmarkNode*)parent
+ configuration:(BookmarkEditor::Configuration)configuration {
+ NSString* nibName = @"BookmarkAllTabs";
+ if ((self = [super initWithParentWindow:parentWindow
+ nibName:nibName
+ profile:profile
+ parent:parent
+ configuration:configuration])) {
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ [self setInitialName:
+ l10n_util::GetNSStringWithFixup(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME)];
+ [super awakeFromNib];
+}
+
+#pragma mark Bookmark Editing
+
+- (void)UpdateActiveTabPairs {
+ activeTabPairsVector_.clear();
+ Browser* browser = BrowserList::GetLastActive();
+ TabStripModel* tabstrip_model = browser->tabstrip_model();
+ const int tabCount = tabstrip_model->count();
+ for (int i = 0; i < tabCount; ++i) {
+ TabContents* tc = tabstrip_model->GetTabContentsAt(i)->tab_contents();
+ const string16 tabTitle = tc->GetTitle();
+ const GURL& tabURL(tc->GetURL());
+ ActiveTabNameURLPair tabPair(tabTitle, tabURL);
+ activeTabPairsVector_.push_back(tabPair);
+ }
+}
+
+// Called by -[BookmarkEditorBaseController ok:]. Creates the container
+// folder for the tabs and then the bookmarks in that new folder.
+// Returns a BOOL as an NSNumber indicating that the commit may proceed.
+- (NSNumber*)didCommit {
+ NSString* name = [[self displayName] stringByTrimmingCharactersInSet:
+ [NSCharacterSet newlineCharacterSet]];
+ std::wstring newTitle = base::SysNSStringToWide(name);
+ const BookmarkNode* newParentNode = [self selectedNode];
+ int newIndex = newParentNode->GetChildCount();
+ // Create the new folder which will contain all of the tab URLs.
+ NSString* newFolderName = [self displayName];
+ string16 newFolderString = base::SysNSStringToUTF16(newFolderName);
+ BookmarkModel* model = [self bookmarkModel];
+ const BookmarkNode* newFolder = model->AddGroup(newParentNode, newIndex,
+ newFolderString);
+ // Get a list of all open tabs, create nodes for them, and add
+ // to the new folder node.
+ [self UpdateActiveTabPairs];
+ int i = 0;
+ for (ActiveTabsNameURLPairVector::const_iterator it =
+ activeTabPairsVector_.begin();
+ it != activeTabPairsVector_.end(); ++it, ++i) {
+ model->AddURL(newFolder, i, it->first, it->second);
+ }
+ return [NSNumber numberWithBool:YES];
+}
+
+- (ActiveTabsNameURLPairVector*)activeTabPairsVector {
+ return &activeTabPairsVector_;
+}
+
+@end // BookmarkAllTabsController
+
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller_unittest.mm
new file mode 100644
index 0000000..9f8a7d8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller_unittest.mm
@@ -0,0 +1,82 @@
+// 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"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface BookmarkAllTabsControllerOverride : BookmarkAllTabsController
+@end
+
+@implementation BookmarkAllTabsControllerOverride
+
+- (void)UpdateActiveTabPairs {
+ ActiveTabsNameURLPairVector* activeTabPairsVector =
+ [self activeTabPairsVector];
+ activeTabPairsVector->clear();
+ activeTabPairsVector->push_back(
+ ActiveTabNameURLPair(ASCIIToUTF16("at-0"), GURL("http://at-0.com")));
+ activeTabPairsVector->push_back(
+ ActiveTabNameURLPair(ASCIIToUTF16("at-1"), GURL("http://at-1.com")));
+ activeTabPairsVector->push_back(
+ ActiveTabNameURLPair(ASCIIToUTF16("at-2"), GURL("http://at-2.com")));
+}
+
+@end
+
+class BookmarkAllTabsControllerTest : public CocoaTest {
+ public:
+ BrowserTestHelper helper_;
+ const BookmarkNode* parent_node_;
+ BookmarkAllTabsControllerOverride* controller_;
+ const BookmarkNode* group_a_;
+
+ BookmarkAllTabsControllerTest() {
+ BookmarkModel& model(*(helper_.profile()->GetBookmarkModel()));
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ group_a_ = model.AddGroup(root, 0, ASCIIToUTF16("a"));
+ model.AddURL(group_a_, 0, ASCIIToUTF16("a-0"), GURL("http://a-0.com"));
+ model.AddURL(group_a_, 1, ASCIIToUTF16("a-1"), GURL("http://a-1.com"));
+ model.AddURL(group_a_, 2, ASCIIToUTF16("a-2"), GURL("http://a-2.com"));
+ }
+
+ virtual BookmarkAllTabsControllerOverride* CreateController() {
+ return [[BookmarkAllTabsControllerOverride alloc]
+ initWithParentWindow:test_window()
+ profile:helper_.profile()
+ parent:group_a_
+ configuration:BookmarkEditor::SHOW_TREE];
+ }
+
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ controller_ = CreateController();
+ [controller_ runAsModalSheet];
+ }
+
+ virtual void TearDown() {
+ controller_ = NULL;
+ CocoaTest::TearDown();
+ }
+};
+
+TEST_F(BookmarkAllTabsControllerTest, BookmarkAllTabs) {
+ // OK button should always be enabled.
+ EXPECT_TRUE([controller_ okButtonEnabled]);
+ [controller_ selectTestNodeInBrowser:group_a_];
+ [controller_ setDisplayName:@"ALL MY TABS"];
+ [controller_ ok:nil];
+ EXPECT_EQ(4, group_a_->GetChildCount());
+ const BookmarkNode* folderChild = group_a_->GetChild(3);
+ EXPECT_EQ(folderChild->GetTitle(), ASCIIToUTF16("ALL MY TABS"));
+ EXPECT_EQ(3, folderChild->GetChildCount());
+}
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h
new file mode 100644
index 0000000..811c450
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h
@@ -0,0 +1,60 @@
+// 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.
+
+// C++ bridge class between Chromium and Cocoa to connect the
+// Bookmarks (model) with the Bookmark Bar (view).
+//
+// There is exactly one BookmarkBarBridge per BookmarkBarController /
+// BrowserWindowController / Browser.
+
+#ifndef CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_BRIDGE_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_BRIDGE_H_
+#pragma once
+
+#include "base/basictypes.h"
+#include "chrome/browser/bookmarks/bookmark_model_observer.h"
+
+class Browser;
+@class BookmarkBarController;
+
+class BookmarkBarBridge : public BookmarkModelObserver {
+ public:
+ BookmarkBarBridge(BookmarkBarController* controller,
+ BookmarkModel* model);
+ virtual ~BookmarkBarBridge();
+
+ // Overridden from BookmarkModelObserver
+ virtual void Loaded(BookmarkModel* model);
+ virtual void BookmarkModelBeingDeleted(BookmarkModel* model);
+ virtual void BookmarkNodeMoved(BookmarkModel* model,
+ const BookmarkNode* old_parent,
+ int old_index,
+ const BookmarkNode* new_parent,
+ int new_index);
+ virtual void BookmarkNodeAdded(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int index);
+ virtual void BookmarkNodeRemoved(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int old_index,
+ const BookmarkNode* node);
+ virtual void BookmarkNodeChanged(BookmarkModel* model,
+ const BookmarkNode* node);
+ virtual void BookmarkNodeFavIconLoaded(BookmarkModel* model,
+ const BookmarkNode* node);
+ virtual void BookmarkNodeChildrenReordered(BookmarkModel* model,
+ const BookmarkNode* node);
+
+ virtual void BookmarkImportBeginning(BookmarkModel* model);
+ virtual void BookmarkImportEnding(BookmarkModel* model);
+
+ private:
+ BookmarkBarController* controller_; // weak; owns me
+ BookmarkModel* model_; // weak; it is owned by a Profile.
+ bool batch_mode_;
+
+ DISALLOW_COPY_AND_ASSIGN(BookmarkBarBridge);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_BRIDGE_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.mm
new file mode 100644
index 0000000..54f5e81
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.mm
@@ -0,0 +1,82 @@
+// 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 "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h"
+
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+
+BookmarkBarBridge::BookmarkBarBridge(BookmarkBarController* controller,
+ BookmarkModel* model)
+ : controller_(controller),
+ model_(model),
+ batch_mode_(false) {
+ model_->AddObserver(this);
+
+ // Bookmark loading is async; it may may not have happened yet.
+ // We will be notified when that happens with the AddObserver() call.
+ if (model->IsLoaded())
+ Loaded(model);
+}
+
+BookmarkBarBridge::~BookmarkBarBridge() {
+ model_->RemoveObserver(this);
+}
+
+void BookmarkBarBridge::Loaded(BookmarkModel* model) {
+ [controller_ loaded:model];
+}
+
+void BookmarkBarBridge::BookmarkModelBeingDeleted(BookmarkModel* model) {
+ [controller_ beingDeleted:model];
+}
+
+void BookmarkBarBridge::BookmarkNodeMoved(BookmarkModel* model,
+ const BookmarkNode* old_parent,
+ int old_index,
+ const BookmarkNode* new_parent,
+ int new_index) {
+ [controller_ nodeMoved:model
+ oldParent:old_parent oldIndex:old_index
+ newParent:new_parent newIndex:new_index];
+}
+
+void BookmarkBarBridge::BookmarkNodeAdded(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int index) {
+ if (!batch_mode_) {
+ [controller_ nodeAdded:model parent:parent index:index];
+ }
+}
+
+void BookmarkBarBridge::BookmarkNodeRemoved(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int old_index,
+ const BookmarkNode* node) {
+ [controller_ nodeRemoved:model parent:parent index:old_index];
+}
+
+void BookmarkBarBridge::BookmarkNodeChanged(BookmarkModel* model,
+ const BookmarkNode* node) {
+ [controller_ nodeChanged:model node:node];
+}
+
+void BookmarkBarBridge::BookmarkNodeFavIconLoaded(BookmarkModel* model,
+ const BookmarkNode* node) {
+ [controller_ nodeFavIconLoaded:model node:node];
+}
+
+void BookmarkBarBridge::BookmarkNodeChildrenReordered(
+ BookmarkModel* model, const BookmarkNode* node) {
+ [controller_ nodeChildrenReordered:model node:node];
+}
+
+void BookmarkBarBridge::BookmarkImportBeginning(BookmarkModel* model) {
+ batch_mode_ = true;
+}
+
+void BookmarkBarBridge::BookmarkImportEnding(BookmarkModel* model) {
+ batch_mode_ = false;
+ [controller_ loaded:model];
+}
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge_unittest.mm
new file mode 100644
index 0000000..067d327
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge_unittest.mm
@@ -0,0 +1,135 @@
+// 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 "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h"
+#include "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+// TODO(jrg): use OCMock.
+
+namespace {
+
+// Information needed to open a URL, as passed to the
+// BookmarkBarController's delegate.
+typedef std::pair<GURL,WindowOpenDisposition> OpenInfo;
+
+} // The namespace must end here -- I need to use OpenInfo in
+ // FakeBookmarkBarController but can't place
+ // FakeBookmarkBarController itself in the namespace ("error:
+ // Objective-C declarations may only appear in global scope")
+
+// Oddly, we are our own delegate.
+@interface FakeBookmarkBarController : BookmarkBarController {
+ @public
+ scoped_nsobject<NSMutableArray> callbacks_;
+ std::vector<OpenInfo> opens_;
+}
+@end
+
+@implementation FakeBookmarkBarController
+
+- (id)initWithBrowser:(Browser*)browser {
+ if ((self = [super initWithBrowser:browser
+ initialWidth:100 // arbitrary
+ delegate:nil
+ resizeDelegate:nil])) {
+ callbacks_.reset([[NSMutableArray alloc] init]);
+ }
+ return self;
+}
+
+- (void)loaded:(BookmarkModel*)model {
+ [callbacks_ addObject:[NSNumber numberWithInt:0]];
+}
+
+- (void)beingDeleted:(BookmarkModel*)model {
+ [callbacks_ addObject:[NSNumber numberWithInt:1]];
+}
+
+- (void)nodeMoved:(BookmarkModel*)model
+ oldParent:(const BookmarkNode*)oldParent oldIndex:(int)oldIndex
+ newParent:(const BookmarkNode*)newParent newIndex:(int)newIndex {
+ [callbacks_ addObject:[NSNumber numberWithInt:2]];
+}
+
+- (void)nodeAdded:(BookmarkModel*)model
+ parent:(const BookmarkNode*)oldParent index:(int)index {
+ [callbacks_ addObject:[NSNumber numberWithInt:3]];
+}
+
+- (void)nodeChanged:(BookmarkModel*)model
+ node:(const BookmarkNode*)node {
+ [callbacks_ addObject:[NSNumber numberWithInt:4]];
+}
+
+- (void)nodeFavIconLoaded:(BookmarkModel*)model
+ node:(const BookmarkNode*)node {
+ [callbacks_ addObject:[NSNumber numberWithInt:5]];
+}
+
+- (void)nodeChildrenReordered:(BookmarkModel*)model
+ node:(const BookmarkNode*)node {
+ [callbacks_ addObject:[NSNumber numberWithInt:6]];
+}
+
+- (void)nodeRemoved:(BookmarkModel*)model
+ parent:(const BookmarkNode*)oldParent index:(int)index {
+ [callbacks_ addObject:[NSNumber numberWithInt:7]];
+}
+
+// Save the request.
+- (void)openBookmarkURL:(const GURL&)url
+ disposition:(WindowOpenDisposition)disposition {
+ opens_.push_back(OpenInfo(url, disposition));
+}
+
+@end
+
+
+class BookmarkBarBridgeTest : public CocoaTest {
+ public:
+ BrowserTestHelper browser_test_helper_;
+};
+
+// Call all the callbacks; make sure they are all redirected to the objc object.
+TEST_F(BookmarkBarBridgeTest, TestRedirect) {
+ Browser* browser = browser_test_helper_.browser();
+ Profile* profile = browser_test_helper_.profile();
+ BookmarkModel* model = profile->GetBookmarkModel();
+
+ scoped_nsobject<NSView> parentView([[NSView alloc]
+ initWithFrame:NSMakeRect(0,0,100,100)]);
+ scoped_nsobject<NSView> webView([[NSView alloc]
+ initWithFrame:NSMakeRect(0,0,100,100)]);
+ scoped_nsobject<NSView> infoBarsView(
+ [[NSView alloc] initWithFrame:NSMakeRect(0,0,100,100)]);
+
+ scoped_nsobject<FakeBookmarkBarController>
+ controller([[FakeBookmarkBarController alloc] initWithBrowser:browser]);
+ EXPECT_TRUE(controller.get());
+ scoped_ptr<BookmarkBarBridge> bridge(new BookmarkBarBridge(controller.get(),
+ model));
+ EXPECT_TRUE(bridge.get());
+
+ bridge->Loaded(NULL);
+ bridge->BookmarkModelBeingDeleted(NULL);
+ bridge->BookmarkNodeMoved(NULL, NULL, 0, NULL, 0);
+ bridge->BookmarkNodeAdded(NULL, NULL, 0);
+ bridge->BookmarkNodeChanged(NULL, NULL);
+ bridge->BookmarkNodeFavIconLoaded(NULL, NULL);
+ bridge->BookmarkNodeChildrenReordered(NULL, NULL);
+ bridge->BookmarkNodeRemoved(NULL, NULL, 0, NULL);
+
+ // 8 calls above plus an initial Loaded() in init routine makes 9
+ EXPECT_TRUE([controller.get()->callbacks_ count] == 9);
+
+ for (int x = 1; x < 9; x++) {
+ NSNumber* num = [NSNumber numberWithInt:x-1];
+ EXPECT_NSEQ(num, [controller.get()->callbacks_ objectAtIndex:x]);
+ }
+}
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h
new file mode 100644
index 0000000..b4e4bef
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h
@@ -0,0 +1,38 @@
+// 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.
+
+// Constants used for positioning the bookmark bar. These aren't placed in a
+// different file because they're conditionally included in cross platform code
+// and thus no Objective-C++ stuff.
+
+#ifndef CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_CONSTANTS_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_CONSTANTS_H_
+#pragma once
+
+namespace bookmarks {
+
+// Correction used for computing other values based on the height.
+const int kVisualHeightOffset = 2;
+
+// Bar height, when opened in "always visible" mode. This is actually a little
+// smaller than it should be (by |kVisualHeightOffset| points) because of the
+// visual overlap with the main toolbar. When using this to compute values
+// other than the actual height of the toolbar, be sure to add
+// |kVisualHeightOffset|.
+const int kBookmarkBarHeight = 26;
+
+// Our height, when visible in "new tab page" mode.
+const int kNTPBookmarkBarHeight = 40;
+
+// The amount of space between the inner bookmark bar and the outer toolbar on
+// new tab pages.
+const int kNTPBookmarkBarPadding =
+ (kNTPBookmarkBarHeight - (kBookmarkBarHeight + kVisualHeightOffset)) / 2;
+
+// The height of buttons in the bookmark bar.
+const int kBookmarkButtonHeight = kBookmarkBarHeight + kVisualHeightOffset;
+
+} // namespace bookmarks
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_CONSTANTS_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h
new file mode 100644
index 0000000..a9bca8a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h
@@ -0,0 +1,399 @@
+// 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_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_CONTROLLER_H_
+#pragma once
+
+#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/ui/cocoa/bookmarks/bookmark_bar_bridge.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_state.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
+#include "chrome/browser/ui/cocoa/tab_strip_model_observer_bridge.h"
+#include "webkit/glue/window_open_disposition.h"
+
+@class BookmarkBarController;
+@class BookmarkBarFolderController;
+@class BookmarkBarView;
+@class BookmarkButton;
+@class BookmarkButtonCell;
+@class BookmarkFolderTarget;
+class BookmarkModel;
+@class BookmarkMenu;
+class BookmarkNode;
+class Browser;
+class GURL;
+class PrefService;
+class TabContents;
+@class ToolbarController;
+@protocol ViewResizer;
+
+namespace bookmarks {
+
+// Magic numbers from Cole
+// TODO(jrg): create an objc-friendly version of bookmark_bar_constants.h?
+
+// Used as a maximum width for buttons on the bar.
+const CGFloat kDefaultBookmarkWidth = 150.0;
+
+// Horizontal frame inset for buttons in the bookmark bar.
+const CGFloat kBookmarkHorizontalPadding = 1.0;
+
+// Vertical frame inset for buttons in the bookmark bar.
+const CGFloat kBookmarkVerticalPadding = 2.0;
+
+// Used as a min/max width for buttons on menus (not on the bar).
+const CGFloat kBookmarkMenuButtonMinimumWidth = 100.0;
+const CGFloat kBookmarkMenuButtonMaximumWidth = 485.0;
+
+// Horizontal separation between a menu button and both edges of its menu.
+const CGFloat kBookmarkSubMenuHorizontalPadding = 5.0;
+
+// TODO(mrossetti): Add constant (kBookmarkVerticalSeparation) for the gap
+// between buttons in a folder menu. Right now we're using
+// kBookmarkVerticalPadding, which is dual purpose and wrong.
+// http://crbug.com/59057
+
+// Convenience constant giving the vertical distance from the top extent of one
+// folder button to the next button.
+const CGFloat kBookmarkButtonVerticalSpan =
+ kBookmarkButtonHeight + kBookmarkVerticalPadding;
+
+// The minimum separation between a folder menu and the edge of the screen.
+// If the menu gets closer to the edge of the screen (either right or left)
+// then it is pops up in the opposite direction.
+// (See -[BookmarkBarFolderController childFolderWindowLeftForWidth:]).
+const CGFloat kBookmarkHorizontalScreenPadding = 8.0;
+
+// Our NSScrollView is supposed to be just barely big enough to fit its
+// contentView. It is actually a hair too small.
+// This turns on horizontal scrolling which, although slight, is awkward.
+// Make sure our window (and NSScrollView) are wider than its documentView
+// by at least this much.
+const CGFloat kScrollViewContentWidthMargin = 2;
+
+// Make subfolder menus overlap their parent menu a bit to give a better
+// perception of a menuing system.
+const CGFloat kBookmarkMenuOverlap = 5.0;
+
+// 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
+// delegate is the BWC and is responsible for ensuring that the toolbar is
+// displayed correctly (as specified by |-getDesiredToolbarHeightCompression|
+// and |-toolbarDividerOpacity|) at the beginning and at the end of an animation
+// (or after a state change).
+@protocol BookmarkBarControllerDelegate
+
+// Sent when the state has changed (after any animation), but before the final
+// display update.
+- (void)bookmarkBar:(BookmarkBarController*)controller
+ didChangeFromState:(bookmarks::VisualState)oldState
+ toState:(bookmarks::VisualState)newState;
+
+// Sent before the animation begins.
+- (void)bookmarkBar:(BookmarkBarController*)controller
+willAnimateFromState:(bookmarks::VisualState)oldState
+ toState:(bookmarks::VisualState)newState;
+
+@end
+
+// A controller for the bookmark bar in the browser window. Handles showing
+// and hiding based on the preference in the given profile.
+@interface BookmarkBarController :
+ NSViewController<BookmarkBarState,
+ BookmarkBarToolbarViewController,
+ BookmarkButtonDelegate,
+ BookmarkButtonControllerProtocol,
+ CrApplicationEventHookProtocol,
+ NSUserInterfaceValidations> {
+ @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"
+ // state. This is set to |kInvalidState| on initialization (when the
+ // appropriate state is not yet known).
+ bookmarks::VisualState visualState_;
+
+ // The "original" state of the bookmark bar if an animation is running,
+ // otherwise it should be |kInvalidState|.
+ bookmarks::VisualState lastVisualState_;
+
+ Browser* browser_; // weak; owned by its window
+ BookmarkModel* bookmarkModel_; // weak; part of the profile owned by the
+ // top-level Browser object.
+
+ // Our initial view width, which is applied in awakeFromNib.
+ CGFloat initialWidth_;
+
+ // BookmarkNodes have a 64bit id. NSMenuItems have a 32bit tag used
+ // to represent the bookmark node they refer to. This map provides
+ // a mapping from one to the other, so we can properly identify the
+ // node from the item. When adding items in, we start with seedId_.
+ int32 seedId_;
+ std::map<int32,int64> menuTagMap_;
+
+ // Our bookmark buttons, ordered from L-->R.
+ scoped_nsobject<NSMutableArray> buttons_;
+
+ // The folder image so we can use one copy for all buttons
+ scoped_nsobject<NSImage> folderImage_;
+
+ // The default image, so we can use one copy for all buttons.
+ scoped_nsobject<NSImage> defaultImage_;
+
+ // If the bar is disabled, we hide it and ignore show/hide commands.
+ // Set when using fullscreen mode.
+ BOOL barIsEnabled_;
+
+ // Bridge from Chrome-style C++ notifications (e.g. derived from
+ // BookmarkModelObserver)
+ scoped_ptr<BookmarkBarBridge> bridge_;
+
+ // Delegate that is informed about state changes in the bookmark bar.
+ id<BookmarkBarControllerDelegate> delegate_; // weak
+
+ // Delegate that can resize us.
+ id<ViewResizer> resizeDelegate_; // weak
+
+ // Logic for dealing with a click on a bookmark folder button.
+ scoped_nsobject<BookmarkFolderTarget> folderTarget_;
+
+ // 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_;
+
+ // Are watching for a "click outside" or other event which would
+ // signal us to close the bookmark bar folder menus?
+ BOOL watchingForExitEvent_;
+
+ IBOutlet BookmarkBarView* buttonView_; // Contains 'no items' text fields.
+ IBOutlet BookmarkButton* offTheSideButton_; // aka the chevron.
+ IBOutlet NSMenu* buttonContextMenu_;
+
+ NSRect originalNoItemsRect_; // Original, pre-resized field rect.
+ NSRect originalImportBookmarksRect_; // Original, pre-resized field rect.
+
+ // "Other bookmarks" button on the right side.
+ scoped_nsobject<BookmarkButton> otherBookmarksButton_;
+
+ // 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_;
+
+ // We save the view width when we add bookmark buttons. This lets
+ // us avoid a rebuild until we've grown the window bigger than our
+ // initial build.
+ CGFloat savedFrameWidth_;
+
+ // The number of buttons we display in the bookmark bar. This does
+ // not include the "off the side" chevron or the "Other Bookmarks"
+ // button. We use this number to determine if we need to display
+ // the chevron, and to know what to place in the chevron's menu.
+ // Since we create everything before doing layout we can't be sure
+ // that all bookmark buttons we create will be visible. Thus,
+ // [buttons_ count] isn't a definitive check.
+ int displayedButtonCount_;
+
+ // A state flag which tracks when the bar's folder menus should be shown.
+ // An initial click in any of the folder buttons turns this on and
+ // one of the following will turn it off: another click in the button,
+ // the window losing focus, a click somewhere other than in the bar
+ // or a folder menu.
+ BOOL showFolderMenus_;
+
+ // Set to YES to prevent any node animations. Useful for unit testing so that
+ // incomplete animations do not cause valgrind complaints.
+ BOOL ignoreAnimations_;
+}
+
+@property(readonly, nonatomic) bookmarks::VisualState visualState;
+@property(readonly, nonatomic) bookmarks::VisualState lastVisualState;
+@property(assign, nonatomic) id<BookmarkBarControllerDelegate> delegate;
+
+// Initializes the bookmark bar controller with the given browser
+// profile and delegates.
+- (id)initWithBrowser:(Browser*)browser
+ initialWidth:(CGFloat)initialWidth
+ delegate:(id<BookmarkBarControllerDelegate>)delegate
+ resizeDelegate:(id<ViewResizer>)resizeDelegate;
+
+// Updates the bookmark bar (from its current, possibly in-transition) state to
+// the one appropriate for the new conditions.
+- (void)updateAndShowNormalBar:(BOOL)showNormalBar
+ showDetachedBar:(BOOL)showDetachedBar
+ withAnimation:(BOOL)animate;
+
+// Update the visible state of the bookmark bar.
+- (void)updateVisibility;
+
+// Turn on or off the bookmark bar and prevent or reallow its appearance. On
+// disable, toggle off if shown. On enable, show only if needed. App and popup
+// windows do not show a bookmark bar.
+- (void)setBookmarkBarEnabled:(BOOL)enabled;
+
+// Returns the amount by which the toolbar above should be compressed.
+- (CGFloat)getDesiredToolbarHeightCompression;
+
+// Gets the appropriate opacity for the toolbar's divider; 0 means that it
+// shouldn't be shown.
+- (CGFloat)toolbarDividerOpacity;
+
+// Updates the sizes and positions of the subviews.
+// TODO(viettrungluu): I'm not convinced this should be public, but I currently
+// need it for animations. Try not to propagate its use.
+- (void)layoutSubviews;
+
+// Called by our view when it is moved to a window.
+- (void)viewDidMoveToWindow;
+
+// Import bookmarks from another browser.
+- (IBAction)importBookmarks:(id)sender;
+
+// Provide a favIcon for a bookmark node. May return nil.
+- (NSImage*)favIconForNode:(const BookmarkNode*)node;
+
+// Used for situations where the bookmark bar folder menus should no longer
+// be actively popping up. Called when the window loses focus, a click has
+// occured outside the menus or a bookmark has been activated. (Note that this
+// differs from the behavior of the -[BookmarkButtonControllerProtocol
+// closeAllBookmarkFolders] method in that the latter does not terminate menu
+// tracking since it may be being called in response to actions (such as
+// dragging) where a 'stale' menu presentation should first be collapsed before
+// presenting a new menu.)
+- (void)closeFolderAndStopTrackingMenus;
+
+// Checks if operations such as edit or delete are allowed.
+- (BOOL)canEditBookmark:(const BookmarkNode*)node;
+
+// Actions for manipulating bookmarks.
+// Open a normal bookmark or folder from a button, ...
+- (IBAction)openBookmark:(id)sender;
+- (IBAction)openBookmarkFolderFromButton:(id)sender;
+// From the "off the side" button, ...
+- (IBAction)openOffTheSideFolderFromButton:(id)sender;
+// From a context menu over the button, ...
+- (IBAction)openBookmarkInNewForegroundTab:(id)sender;
+- (IBAction)openBookmarkInNewWindow:(id)sender;
+- (IBAction)openBookmarkInIncognitoWindow:(id)sender;
+- (IBAction)editBookmark:(id)sender;
+- (IBAction)cutBookmark:(id)sender;
+- (IBAction)copyBookmark:(id)sender;
+- (IBAction)pasteBookmark:(id)sender;
+- (IBAction)deleteBookmark:(id)sender;
+// From a context menu over the bar, ...
+- (IBAction)openAllBookmarks:(id)sender;
+- (IBAction)openAllBookmarksNewWindow:(id)sender;
+- (IBAction)openAllBookmarksIncognitoWindow:(id)sender;
+// Or from a context menu over either the bar or a button.
+- (IBAction)addPage:(id)sender;
+- (IBAction)addFolder:(id)sender;
+
+@end
+
+// Redirects from BookmarkBarBridge, the C++ object which glues us to
+// the rest of Chromium. Internal to BookmarkBarController.
+@interface BookmarkBarController(BridgeRedirect)
+- (void)loaded:(BookmarkModel*)model;
+- (void)beingDeleted:(BookmarkModel*)model;
+- (void)nodeAdded:(BookmarkModel*)model
+ parent:(const BookmarkNode*)oldParent index:(int)index;
+- (void)nodeChanged:(BookmarkModel*)model
+ node:(const BookmarkNode*)node;
+- (void)nodeMoved:(BookmarkModel*)model
+ oldParent:(const BookmarkNode*)oldParent oldIndex:(int)oldIndex
+ newParent:(const BookmarkNode*)newParent newIndex:(int)newIndex;
+- (void)nodeRemoved:(BookmarkModel*)model
+ parent:(const BookmarkNode*)oldParent index:(int)index;
+- (void)nodeFavIconLoaded:(BookmarkModel*)model
+ node:(const BookmarkNode*)node;
+- (void)nodeChildrenReordered:(BookmarkModel*)model
+ node:(const BookmarkNode*)node;
+@end
+
+// These APIs should only be used by unit tests (or used internally).
+@interface BookmarkBarController(InternalOrTestingAPI)
+- (BookmarkBarView*)buttonView;
+- (NSMutableArray*)buttons;
+- (NSMenu*)offTheSideMenu;
+- (NSButton*)offTheSideButton;
+- (BOOL)offTheSideButtonIsHidden;
+- (BookmarkButton*)otherBookmarksButton;
+- (BookmarkBarFolderController*)folderController;
+- (id)folderTarget;
+- (int)displayedButtonCount;
+- (void)openURL:(GURL)url disposition:(WindowOpenDisposition)disposition;
+- (void)clearBookmarkBar;
+- (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)node;
+- (NSRect)frameForBookmarkButtonFromCell:(NSCell*)cell xOffset:(int*)xOffset;
+- (void)checkForBookmarkButtonGrowth:(NSButton*)button;
+- (void)frameDidChange;
+- (int64)nodeIdFromMenuTag:(int32)tag;
+- (int32)menuTagFromNodeId:(int64)menuid;
+- (const BookmarkNode*)nodeFromMenuItem:(id)sender;
+- (void)updateTheme:(ThemeProvider*)themeProvider;
+- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point;
+- (BOOL)isEventAnExitEvent:(NSEvent*)event;
+- (BOOL)shrinkOrHideView:(NSView*)view forMaxX:(CGFloat)maxViewX;
+
+// The following are for testing purposes only and are not used internally.
+- (NSMenu *)menuForFolderNode:(const BookmarkNode*)node;
+- (NSMenu*)buttonContextMenu;
+- (void)setButtonContextMenu:(id)menu;
+// Set to YES in order to prevent animations.
+- (void)setIgnoreAnimations:(BOOL)ignore;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.mm
new file mode 100644
index 0000000..f8ed23f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.mm
@@ -0,0 +1,2497 @@
+// 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/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+
+#include "app/l10n_util_mac.h"
+#include "app/resource_bundle.h"
+#include "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_editor.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/bookmarks/bookmark_utils.h"
+#include "chrome/browser/metrics/user_metrics.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents/tab_contents_view.h"
+#import "chrome/browser/themes/browser_theme_provider.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list.h"
+#import "chrome/browser/ui/cocoa/background_gradient_view.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/event_utils.h"
+#import "chrome/browser/ui/cocoa/fullscreen_controller.h"
+#import "chrome/browser/ui/cocoa/import_settings_dialog.h"
+#import "chrome/browser/ui/cocoa/menu_button.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#import "chrome/browser/ui/cocoa/toolbar_controller.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+#import "chrome/browser/ui/cocoa/view_resizer.h"
+#include "chrome/common/pref_names.h"
+#include "grit/app_resources.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+
+// Bookmark bar state changing and animations
+//
+// The bookmark bar has three real states: "showing" (a normal bar attached to
+// the toolbar), "hidden", and "detached" (pretending to be part of the web
+// content on the NTP). It can, or at least should be able to, animate between
+// these states. There are several complications even without animation:
+// - The placement of the bookmark bar is done by the BWC, and it needs to know
+// the state in order to place the bookmark bar correctly (immediately below
+// the toolbar when showing, below the infobar when detached).
+// - The "divider" (a black line) needs to be drawn by either the toolbar (when
+// the bookmark bar is hidden or detached) or by the bookmark bar (when it is
+// showing). It should not be drawn by both.
+// - The toolbar needs to vertically "compress" when the bookmark bar is
+// showing. This ensures the proper display of both the bookmark bar and the
+// toolbar, and gives a padded area around the bookmark bar items for right
+// clicks, etc.
+//
+// Our model is that the BWC controls us and also the toolbar. We try not to
+// talk to the browser nor the toolbar directly, instead centralizing control in
+// the BWC. The key method by which the BWC controls us is
+// |-updateAndShowNormalBar:showDetachedBar:withAnimation:|. This invokes state
+// changes, and at appropriate times we request that the BWC do things for us
+// via either the resize delegate or our general delegate. If the BWC needs any
+// information about what it should do, or tell the toolbar to do, it can then
+// query us back (e.g., |-isShownAs...|, |-getDesiredToolbarHeightCompression|,
+// |-toolbarDividerOpacity|, etc.).
+//
+// Animation-related complications:
+// - Compression of the toolbar is touchy during animation. It must not be
+// compressed while the bookmark bar is animating to/from showing (from/to
+// hidden), otherwise it would look like the bookmark bar's contents are
+// sliding out of the controls inside the toolbar. As such, we have to make
+// sure that the bookmark bar is shown at the right location and at the
+// right height (at various points in time).
+// - Showing the divider is also complicated during animation between hidden
+// and showing. We have to make sure that the toolbar does not show the
+// divider despite the fact that it's not compressed. The exception to this
+// is at the beginning/end of the animation when the toolbar is still
+// uncompressed but the bookmark bar has height 0. If we're not careful, we
+// get a flicker at this point.
+// - We have to ensure that we do the right thing if we're told to change state
+// while we're running an animation. The generic/easy thing to do is to jump
+// to the end state of our current animation, and (if the new state change
+// again involves an animation) begin the new animation. We can do better
+// than that, however, and sometimes just change the current animation to go
+// to the new end state (e.g., by "reversing" the animation in the showing ->
+// hidden -> showing case). We also have to ensure that demands to
+// immediately change state are always honoured.
+//
+// Pointers to animation logic:
+// - |-moveToVisualState:withAnimation:| starts animations, deciding which ones
+// we know how to handle.
+// - |-doBookmarkBarAnimation| has most of the actual logic.
+// - |-getDesiredToolbarHeightCompression| and |-toolbarDividerOpacity| contain
+// related logic.
+// - The BWC's |-layoutSubviews| needs to know how to position things.
+// - The BWC should implement |-bookmarkBar:didChangeFromState:toState:| and
+// |-bookmarkBar:willAnimateFromState:toState:| in order to inform the
+// toolbar of required changes.
+
+namespace {
+
+// Overlap (in pixels) between the toolbar and the bookmark bar (when showing in
+// normal mode).
+const CGFloat kBookmarkBarOverlap = 3.0;
+
+// Duration of the bookmark bar animations.
+const NSTimeInterval kBookmarkBarAnimationDuration = 0.12;
+
+} // namespace
+
+@interface BookmarkBarController(Private)
+
+// Determines the appropriate state for the given situation.
++ (bookmarks::VisualState)visualStateToShowNormalBar:(BOOL)showNormalBar
+ showDetachedBar:(BOOL)showDetachedBar;
+
+// Moves to the given next state (from the current state), possibly animating.
+// If |animate| is NO, it will stop any running animation and jump to the given
+// state. If YES, it may either (depending on implementation) jump to the end of
+// the current animation and begin the next one, or stop the current animation
+// mid-flight and animate to the next state.
+- (void)moveToVisualState:(bookmarks::VisualState)nextVisualState
+ withAnimation:(BOOL)animate;
+
+// Return the backdrop to the bookmark bar as various types.
+- (BackgroundGradientView*)backgroundGradientView;
+- (AnimatableView*)animatableView;
+
+// Create buttons for all items in the given bookmark node tree.
+// Modifies self->buttons_. Do not add more buttons than will fit on the view.
+- (void)addNodesToButtonList:(const BookmarkNode*)node;
+
+// Create an autoreleased button appropriate for insertion into the bookmark
+// bar. Update |xOffset| with the offset appropriate for the subsequent button.
+- (BookmarkButton*)buttonForNode:(const BookmarkNode*)node
+ xOffset:(int*)xOffset;
+
+// Puts stuff into the final visual state without animating, stopping a running
+// animation if necessary.
+- (void)finalizeVisualState;
+
+// Stops any current animation in its tracks (midway).
+- (void)stopCurrentAnimation;
+
+// Show/hide the bookmark bar.
+// if |animate| is YES, the changes are made using the animator; otherwise they
+// are made immediately.
+- (void)showBookmarkBarWithAnimation:(BOOL)animate;
+
+// Handles animating the resize of the content view. Returns YES if it handled
+// the animation, NO if not (and hence it should be done instantly).
+- (BOOL)doBookmarkBarAnimation;
+
+// |point| is in the base coordinate system of the destination window;
+// it comes from an id<NSDraggingInfo>. |copy| is YES if a copy is to be
+// made and inserted into the new location while leaving the bookmark in
+// the old location, otherwise move the bookmark by removing from its old
+// location and inserting into the new location.
+- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
+ to:(NSPoint)point
+ copy:(BOOL)copy;
+
+// Returns the index in the model for a drag to the location given by
+// |point|. This is determined by finding the first button before the center
+// of which |point| falls, scanning left to right. Note that, currently, only
+// the x-coordinate of |point| is considered. Though not currently implemented,
+// we may check for errors, in which case this would return negative value;
+// callers should check for this.
+- (int)indexForDragToPoint:(NSPoint)point;
+
+// Add or remove buttons to/from the bar until it is filled but not overflowed.
+- (void)redistributeButtonsOnBarAsNeeded;
+
+// Determine the nature of the bookmark bar contents based on the number of
+// buttons showing. If too many then show the off-the-side list, if none
+// then show the no items label.
+- (void)reconfigureBookmarkBar;
+
+- (void)addNode:(const BookmarkNode*)child toMenu:(NSMenu*)menu;
+- (void)addFolderNode:(const BookmarkNode*)node toMenu:(NSMenu*)menu;
+- (void)tagEmptyMenu:(NSMenu*)menu;
+- (void)clearMenuTagMap;
+- (int)preferredHeight;
+- (void)addNonBookmarkButtonsToView;
+- (void)addButtonsToView;
+- (void)centerNoItemsLabel;
+- (void)setNodeForBarMenu;
+
+- (void)watchForExitEvent:(BOOL)watch;
+
+@end
+
+@implementation BookmarkBarController
+
+@synthesize visualState = visualState_;
+@synthesize lastVisualState = lastVisualState_;
+@synthesize delegate = delegate_;
+
+- (id)initWithBrowser:(Browser*)browser
+ initialWidth:(float)initialWidth
+ delegate:(id<BookmarkBarControllerDelegate>)delegate
+ resizeDelegate:(id<ViewResizer>)resizeDelegate {
+ if ((self = [super initWithNibName:@"BookmarkBar"
+ bundle:mac_util::MainAppBundle()])) {
+ // Initialize to an invalid state.
+ visualState_ = bookmarks::kInvalidState;
+ lastVisualState_ = bookmarks::kInvalidState;
+
+ browser_ = browser;
+ initialWidth_ = initialWidth;
+ bookmarkModel_ = browser_->profile()->GetBookmarkModel();
+ buttons_.reset([[NSMutableArray alloc] init]);
+ delegate_ = delegate;
+ resizeDelegate_ = resizeDelegate;
+ folderTarget_.reset([[BookmarkFolderTarget alloc] initWithController:self]);
+
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ folderImage_.reset(
+ [rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER) retain]);
+ defaultImage_.reset([rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON) retain]);
+
+ // Register for theme changes, bookmark button pulsing, ...
+ NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
+ [defaultCenter addObserver:self
+ selector:@selector(themeDidChangeNotification:)
+ name:kBrowserThemeDidChangeNotification
+ object:nil];
+ [defaultCenter addObserver:self
+ selector:@selector(pulseBookmarkNotification:)
+ name:bookmark_button::kPulseBookmarkButtonNotification
+ object:nil];
+
+ // This call triggers an awakeFromNib, which builds the bar, which
+ // might uses folderImage_. So make sure it happens after
+ // folderImage_ is loaded.
+ [[self animatableView] setResizeDelegate:resizeDelegate];
+ }
+ return self;
+}
+
+- (void)pulseBookmarkNotification:(NSNotification*)notification {
+ NSDictionary* dict = [notification userInfo];
+ const BookmarkNode* node = NULL;
+ NSValue *value = [dict objectForKey:bookmark_button::kBookmarkKey];
+ DCHECK(value);
+ if (value)
+ node = static_cast<const BookmarkNode*>([value pointerValue]);
+ NSNumber* number = [dict
+ objectForKey:bookmark_button::kBookmarkPulseFlagKey];
+ DCHECK(number);
+ BOOL doPulse = number ? [number boolValue] : NO;
+
+ // 3 cases:
+ // button on the bar: flash it
+ // button in "other bookmarks" folder: flash other bookmarks
+ // button in "off the side" folder: flash the chevron
+ for (BookmarkButton* button in [self buttons]) {
+ if ([button bookmarkNode] == node) {
+ [button setIsContinuousPulsing:doPulse];
+ return;
+ }
+ }
+ if ([otherBookmarksButton_ bookmarkNode] == node) {
+ [otherBookmarksButton_ setIsContinuousPulsing:doPulse];
+ return;
+ }
+ if (node->GetParent() == bookmarkModel_->GetBookmarkBarNode()) {
+ [offTheSideButton_ setIsContinuousPulsing:doPulse];
+ return;
+ }
+
+ NOTREACHED() << "no bookmark button found to pulse!";
+}
+
+- (void)dealloc {
+ // We better stop any in-flight animation if we're being killed.
+ [[self animatableView] stopAnimation];
+
+ // Remove our view from its superview so it doesn't attempt to reference
+ // it when the controller is gone.
+ //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];
+ [button setTarget:nil];
+ [button setAction:nil];
+ }
+
+ bridge_.reset(NULL);
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [self watchForExitEvent:NO];
+ [super dealloc];
+}
+
+- (void)awakeFromNib {
+ // We default to NOT open, which means height=0.
+ DCHECK([[self view] isHidden]); // Hidden so it's OK to change.
+
+ // Set our initial height to zero, since that is what the superview
+ // expects. We will resize ourselves open later if needed.
+ [[self view] setFrame:NSMakeRect(0, 0, initialWidth_, 0)];
+
+ // Complete init of the "off the side" button, as much as we can.
+ [offTheSideButton_ setDraggable:NO];
+
+ // We are enabled by default.
+ barIsEnabled_ = YES;
+
+ // Remember the original sizes of the 'no items' and 'import bookmarks'
+ // fields to aid in resizing when the window frame changes.
+ originalNoItemsRect_ = [[buttonView_ noItemTextfield] frame];
+ originalImportBookmarksRect_ = [[buttonView_ importBookmarksButton] frame];
+
+ // To make life happier when the bookmark bar is floating, the chevron is a
+ // child of the button view.
+ [offTheSideButton_ removeFromSuperview];
+ [buttonView_ addSubview:offTheSideButton_];
+
+ // Copy the bar menu so we know if it's from the bar or a folder.
+ // Then we set its represented item to be the bookmark bar.
+ buttonFolderContextMenu_.reset([[[self view] menu] copy]);
+
+ // When resized we may need to add new buttons, or remove them (if
+ // 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]];
+
+ // Watch for things going to or from fullscreen.
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(willEnterOrLeaveFullscreen:)
+ name:kWillEnterFullscreenNotification
+ object:nil];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(willEnterOrLeaveFullscreen:)
+ name:kWillLeaveFullscreenNotification
+ object:nil];
+
+ // 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];
+
+ // Remove any existing notifications before registering for new ones.
+ [defaultCenter removeObserver:self
+ name:NSWindowWillCloseNotification
+ object:nil];
+ [defaultCenter removeObserver:self
+ name:NSWindowDidResignKeyNotification
+ object:nil];
+
+ [defaultCenter addObserver:self
+ selector:@selector(parentWindowWillClose:)
+ name:NSWindowWillCloseNotification
+ object:[[self view] window]];
+ [defaultCenter addObserver:self
+ selector:@selector(parentWindowDidResignKey:)
+ name:NSWindowDidResignKeyNotification
+ object:[[self view] window]];
+}
+
+// When going fullscreen we can run into trouble. Our view is removed
+// from the non-fullscreen window before the non-fullscreen window
+// loses key, so our parentDidResignKey: callback never gets called.
+// In addition, a bookmark folder controller needs to be autoreleased
+// (in case it's in the event chain when closed), but the release
+// implicitly needs to happen while it's connected to the original
+// (non-fullscreen) window to "unlock bar visibility". Such a
+// contract isn't honored when going fullscreen with the menu option
+// (not with the keyboard shortcut). We fake it as best we can here.
+// We have a similar problem leaving fullscreen.
+- (void)willEnterOrLeaveFullscreen:(NSNotification*)notification {
+ if (folderController_) {
+ [self childFolderWillClose:folderController_];
+ [self closeFolderAndStopTrackingMenus];
+ }
+}
+
+// NSNotificationCenter callback.
+- (void)parentWindowWillClose:(NSNotification*)notification {
+ [self closeFolderAndStopTrackingMenus];
+}
+
+// NSNotificationCenter callback.
+- (void)parentWindowDidResignKey:(NSNotification*)notification {
+ [self closeFolderAndStopTrackingMenus];
+}
+
+// Change the layout of the bookmark bar's subviews in response to a visibility
+// change (e.g., show or hide the bar) or style change (attached or floating).
+- (void)layoutSubviews {
+ NSRect frame = [[self view] frame];
+ NSRect buttonViewFrame = NSMakeRect(0, 0, NSWidth(frame), NSHeight(frame));
+
+ // The state of our morph (if any); 1 is total bubble, 0 is the regular bar.
+ CGFloat morph = [self detachedMorphProgress];
+
+ // Add padding to the detached bookmark bar.
+ buttonViewFrame = NSInsetRect(buttonViewFrame,
+ morph * bookmarks::kNTPBookmarkBarPadding,
+ morph * bookmarks::kNTPBookmarkBarPadding);
+
+ [buttonView_ setFrame:buttonViewFrame];
+}
+
+// We don't change a preference; we only change visibility. Preference changing
+// (global state) is handled in |BrowserWindowCocoa::ToggleBookmarkBar()|. We
+// simply update based on what we're told.
+- (void)updateVisibility {
+ [self showBookmarkBarWithAnimation:NO];
+}
+
+- (void)setBookmarkBarEnabled:(BOOL)enabled {
+ if (enabled != barIsEnabled_) {
+ barIsEnabled_ = enabled;
+ [self updateVisibility];
+ }
+}
+
+- (CGFloat)getDesiredToolbarHeightCompression {
+ // Some special cases....
+ if (!barIsEnabled_)
+ return 0;
+
+ if ([self isAnimationRunning]) {
+ // No toolbar compression when animating between hidden and showing, nor
+ // between showing and detached.
+ if ([self isAnimatingBetweenState:bookmarks::kHiddenState
+ andState:bookmarks::kShowingState] ||
+ [self isAnimatingBetweenState:bookmarks::kShowingState
+ andState:bookmarks::kDetachedState])
+ return 0;
+
+ // If we ever need any other animation cases, code would go here.
+ }
+
+ return [self isInState:bookmarks::kShowingState] ? kBookmarkBarOverlap : 0;
+}
+
+- (CGFloat)toolbarDividerOpacity {
+ // Some special cases....
+ if ([self isAnimationRunning]) {
+ // In general, the toolbar shouldn't show a divider while we're animating
+ // between showing and hidden. The exception is when our height is < 1, in
+ // which case we can't draw it. It's all-or-nothing (no partial opacity).
+ if ([self isAnimatingBetweenState:bookmarks::kHiddenState
+ andState:bookmarks::kShowingState])
+ return (NSHeight([[self view] frame]) < 1) ? 1 : 0;
+
+ // The toolbar should show the divider when animating between showing and
+ // detached (but opacity will vary).
+ if ([self isAnimatingBetweenState:bookmarks::kShowingState
+ andState:bookmarks::kDetachedState])
+ return static_cast<CGFloat>([self detachedMorphProgress]);
+
+ // If we ever need any other animation cases, code would go here.
+ }
+
+ // In general, only show the divider when it's in the normal showing state.
+ return [self isInState:bookmarks::kShowingState] ? 0 : 1;
+}
+
+- (NSImage*)favIconForNode:(const BookmarkNode*)node {
+ if (!node)
+ return defaultImage_;
+
+ if (node->is_folder())
+ return folderImage_;
+
+ const SkBitmap& favIcon = bookmarkModel_->GetFavIcon(node);
+ if (!favIcon.isNull())
+ return gfx::SkBitmapToNSImage(favIcon);
+
+ return defaultImage_;
+}
+
+- (void)closeFolderAndStopTrackingMenus {
+ showFolderMenus_ = NO;
+ [self closeAllBookmarkFolders];
+}
+
+- (BOOL)canEditBookmark:(const BookmarkNode*)node {
+ // Don't allow edit/delete of the bar node, or of "Other Bookmarks"
+ if ((node == nil) ||
+ (node == bookmarkModel_->other_node()) ||
+ (node == bookmarkModel_->GetBookmarkBarNode()))
+ return NO;
+ return YES;
+}
+
+#pragma mark Actions
+
+- (IBAction)openBookmark:(id)sender {
+ [self closeFolderAndStopTrackingMenus];
+ DCHECK([sender respondsToSelector:@selector(bookmarkNode)]);
+ const BookmarkNode* node = [sender bookmarkNode];
+ WindowOpenDisposition disposition =
+ event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
+ [self openURL:node->GetURL() disposition:disposition];
+}
+
+// Redirect to our logic shared with BookmarkBarFolderController.
+- (IBAction)openBookmarkFolderFromButton:(id)sender {
+ if (sender != offTheSideButton_) {
+ // Toggle presentation of bar folder menus.
+ showFolderMenus_ = !showFolderMenus_;
+ [folderTarget_ openBookmarkFolderFromButton:sender];
+ } else {
+ // Off-the-side requires special handling.
+ [self openOffTheSideFolderFromButton:sender];
+ }
+}
+
+// The button that sends this one is special; the "off the side"
+// button (chevron) opens like a folder button but isn't exactly a
+// parent folder.
+- (IBAction)openOffTheSideFolderFromButton:(id)sender {
+ DCHECK([sender isKindOfClass:[BookmarkButton class]]);
+ DCHECK([[sender cell] isKindOfClass:[BookmarkButtonCell class]]);
+ [[sender cell] setStartingChildIndex:displayedButtonCount_];
+ [folderTarget_ openBookmarkFolderFromButton:sender];
+}
+
+- (IBAction)openBookmarkInNewForegroundTab:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node)
+ [self openURL:node->GetURL() disposition:NEW_FOREGROUND_TAB];
+ [self closeAllBookmarkFolders];
+}
+
+- (IBAction)openBookmarkInNewWindow:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node)
+ [self openURL:node->GetURL() disposition:NEW_WINDOW];
+}
+
+- (IBAction)openBookmarkInIncognitoWindow:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node)
+ [self openURL:node->GetURL() disposition:OFF_THE_RECORD];
+}
+
+- (IBAction)editBookmark:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (!node)
+ return;
+
+ if (node->is_folder()) {
+ BookmarkNameFolderController* controller =
+ [[BookmarkNameFolderController alloc]
+ initWithParentWindow:[[self view] window]
+ profile:browser_->profile()
+ node:node];
+ [controller runAsModalSheet];
+ return;
+ }
+
+ // There is no real need to jump to a platform-common routine at
+ // this point (which just jumps back to objc) other than consistency
+ // across platforms.
+ //
+ // TODO(jrg): identify when we NO_TREE. I can see it in the code
+ // for the other platforms but can't find a way to trigger it in the
+ // UI.
+ BookmarkEditor::Show([[self view] window],
+ browser_->profile(),
+ node->GetParent(),
+ BookmarkEditor::EditDetails(node),
+ BookmarkEditor::SHOW_TREE);
+}
+
+- (IBAction)cutBookmark:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node) {
+ std::vector<const BookmarkNode*> nodes;
+ nodes.push_back(node);
+ bookmark_utils::CopyToClipboard(bookmarkModel_, nodes, true);
+ }
+}
+
+- (IBAction)copyBookmark:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node) {
+ std::vector<const BookmarkNode*> nodes;
+ nodes.push_back(node);
+ bookmark_utils::CopyToClipboard(bookmarkModel_, nodes, false);
+ }
+}
+
+// Paste the copied node immediately after the node for which the context
+// menu has been presented if the node is a non-folder bookmark, otherwise
+// past at the end of the folder node.
+- (IBAction)pasteBookmark:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node) {
+ int index = -1;
+ if (node != bookmarkModel_->GetBookmarkBarNode() && !node->is_folder()) {
+ const BookmarkNode* parent = node->GetParent();
+ index = parent->IndexOfChild(node) + 1;
+ if (index > parent->GetChildCount())
+ index = -1;
+ node = parent;
+ }
+ bookmark_utils::PasteFromClipboard(bookmarkModel_, node, index);
+ }
+}
+
+- (IBAction)deleteBookmark:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node) {
+ bookmarkModel_->Remove(node->GetParent(),
+ node->GetParent()->IndexOfChild(node));
+ }
+}
+
+- (IBAction)openAllBookmarks:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node) {
+ [self openAll:node disposition:NEW_FOREGROUND_TAB];
+ UserMetrics::RecordAction(UserMetricsAction("OpenAllBookmarks"),
+ browser_->profile());
+ }
+}
+
+- (IBAction)openAllBookmarksNewWindow:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node) {
+ [self openAll:node disposition:NEW_WINDOW];
+ UserMetrics::RecordAction(UserMetricsAction("OpenAllBookmarksNewWindow"),
+ browser_->profile());
+ }
+}
+
+- (IBAction)openAllBookmarksIncognitoWindow:(id)sender {
+ const BookmarkNode* node = [self nodeFromMenuItem:sender];
+ if (node) {
+ [self openAll:node disposition:OFF_THE_RECORD];
+ UserMetrics::RecordAction(
+ UserMetricsAction("OpenAllBookmarksIncognitoWindow"),
+ browser_->profile());
+ }
+}
+
+// May be called from the bar or from a folder button.
+// If called from a button, that button becomes the parent.
+- (IBAction)addPage:(id)sender {
+ const BookmarkNode* parent = [self nodeFromMenuItem:sender];
+ if (!parent)
+ parent = bookmarkModel_->GetBookmarkBarNode();
+ BookmarkEditor::Show([[self view] window],
+ browser_->profile(),
+ parent,
+ BookmarkEditor::EditDetails(),
+ BookmarkEditor::SHOW_TREE);
+}
+
+// Might be called from the context menu over the bar OR over a
+// button. If called from a button, that button becomes a sibling of
+// the new node. If called from the bar, add to the end of the bar.
+- (IBAction)addFolder:(id)sender {
+ const BookmarkNode* senderNode = [self nodeFromMenuItem:sender];
+ const BookmarkNode* parent = NULL;
+ int newIndex = 0;
+ // If triggered from the bar, folder or "others" folder - add as a child to
+ // the end.
+ // If triggered from a bookmark, add as next sibling.
+ BookmarkNode::Type type = senderNode->type();
+ if (type == BookmarkNode::BOOKMARK_BAR ||
+ type == BookmarkNode::OTHER_NODE ||
+ type == BookmarkNode::FOLDER) {
+ parent = senderNode;
+ newIndex = parent->GetChildCount();
+ } else {
+ parent = senderNode->GetParent();
+ newIndex = parent->IndexOfChild(senderNode) + 1;
+ }
+ BookmarkNameFolderController* controller =
+ [[BookmarkNameFolderController alloc]
+ initWithParentWindow:[[self view] window]
+ profile:browser_->profile()
+ parent:parent
+ newIndex:newIndex];
+ [controller runAsModalSheet];
+}
+
+- (IBAction)importBookmarks:(id)sender {
+ [ImportSettingsDialogController showImportSettingsDialogForProfile:
+ browser_->profile()];
+}
+
+#pragma mark Private Methods
+
+// Called after the current theme has changed.
+- (void)themeDidChangeNotification:(NSNotification*)aNotification {
+ ThemeProvider* themeProvider =
+ static_cast<ThemeProvider*>([[aNotification object] pointerValue]);
+ [self updateTheme:themeProvider];
+}
+
+// (Private) Method is the same as [self view], but is provided to be explicit.
+- (BackgroundGradientView*)backgroundGradientView {
+ DCHECK([[self view] isKindOfClass:[BackgroundGradientView class]]);
+ return (BackgroundGradientView*)[self view];
+}
+
+// (Private) Method is the same as [self view], but is provided to be explicit.
+- (AnimatableView*)animatableView {
+ DCHECK([[self view] isKindOfClass:[AnimatableView class]]);
+ return (AnimatableView*)[self view];
+}
+
+// Position the off-the-side chevron to the left of the otherBookmarks button.
+- (void)positionOffTheSideButton {
+ NSRect frame = [offTheSideButton_ frame];
+ if (otherBookmarksButton_.get()) {
+ frame.origin.x = ([otherBookmarksButton_ frame].origin.x -
+ (frame.size.width +
+ bookmarks::kBookmarkHorizontalPadding));
+ [offTheSideButton_ setFrame:frame];
+ }
+}
+
+// Configure the off-the-side button (e.g. specify the node range,
+// check if we should enable or disable it, etc).
+- (void)configureOffTheSideButtonContentsAndVisibility {
+ // If deleting a button while off-the-side is open, buttons may be
+ // promoted from off-the-side to the bar. Accomodate.
+ if (folderController_ &&
+ ([folderController_ parentButton] == offTheSideButton_)) {
+ [folderController_ reconfigureMenu];
+ }
+
+ [[offTheSideButton_ cell] setStartingChildIndex:displayedButtonCount_];
+ [[offTheSideButton_ cell]
+ setBookmarkNode:bookmarkModel_->GetBookmarkBarNode()];
+ int bookmarkChildren = bookmarkModel_->GetBookmarkBarNode()->GetChildCount();
+ if (bookmarkChildren > displayedButtonCount_) {
+ [offTheSideButton_ setHidden:NO];
+ } else {
+ // If we just deleted the last item in an off-the-side menu so the
+ // button will be going away, make sure the menu goes away.
+ if (folderController_ &&
+ ([folderController_ parentButton] == offTheSideButton_))
+ [self closeAllBookmarkFolders];
+ // (And hide the button, too.)
+ [offTheSideButton_ setHidden:YES];
+ }
+}
+
+// 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)watchForExitEvent:(BOOL)watch {
+ CrApplication* app = static_cast<CrApplication*>([NSApplication
+ sharedApplication]);
+ DCHECK([app isKindOfClass:[CrApplication class]]);
+ if (watch) {
+ if (!watchingForExitEvent_)
+ [app addEventHook:self];
+ } else {
+ if (watchingForExitEvent_)
+ [app removeEventHook:self];
+ }
+ watchingForExitEvent_ = watch;
+}
+
+// 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 is unflipped. Also, we want the label to be a fixed distance from
+ // the bottom, so that it slides up properly (on animating to hidden).
+ // The textfield sits in the itemcontainer, so to center it we maintain
+ // equal vertical padding on the top and bottom.
+ int yoffset = (NSHeight([[buttonView_ noItemTextfield] frame]) -
+ NSHeight([[buttonView_ noItemContainer] frame])) / 2;
+ [[buttonView_ noItemContainer] setFrameOrigin:NSMakePoint(0, yoffset)];
+}
+
+// (Private)
+- (void)showBookmarkBarWithAnimation:(BOOL)animate {
+ if (animate && !ignoreAnimations_) {
+ // If |-doBookmarkBarAnimation| does the animation, we're done.
+ if ([self doBookmarkBarAnimation])
+ return;
+
+ // Else fall through and do the change instantly.
+ }
+
+ // Set our height.
+ [resizeDelegate_ resizeView:[self view]
+ newHeight:[self preferredHeight]];
+
+ // Only show the divider if showing the normal bookmark bar.
+ BOOL showsDivider = [self isInState:bookmarks::kShowingState];
+ [[self backgroundGradientView] setShowsDivider:showsDivider];
+
+ // Make sure we're shown.
+ [[self view] setHidden:![self isVisible]];
+
+ // Update everything else.
+ [self layoutSubviews];
+ [self frameDidChange];
+}
+
+// (Private)
+- (BOOL)doBookmarkBarAnimation {
+ if ([self isAnimatingFromState:bookmarks::kHiddenState
+ toState:bookmarks::kShowingState]) {
+ [[self backgroundGradientView] setShowsDivider:YES];
+ [[self view] setHidden:NO];
+ AnimatableView* view = [self animatableView];
+ // Height takes into account the extra height we have since the toolbar
+ // only compresses when we're done.
+ [view animateToNewHeight:(bookmarks::kBookmarkBarHeight -
+ kBookmarkBarOverlap)
+ duration:kBookmarkBarAnimationDuration];
+ } else if ([self isAnimatingFromState:bookmarks::kShowingState
+ toState:bookmarks::kHiddenState]) {
+ [[self backgroundGradientView] setShowsDivider:YES];
+ [[self view] setHidden:NO];
+ AnimatableView* view = [self animatableView];
+ [view animateToNewHeight:0
+ duration:kBookmarkBarAnimationDuration];
+ } else if ([self isAnimatingFromState:bookmarks::kShowingState
+ toState:bookmarks::kDetachedState]) {
+ [[self backgroundGradientView] setShowsDivider:YES];
+ [[self view] setHidden:NO];
+ AnimatableView* view = [self animatableView];
+ [view animateToNewHeight:bookmarks::kNTPBookmarkBarHeight
+ duration:kBookmarkBarAnimationDuration];
+ } else if ([self isAnimatingFromState:bookmarks::kDetachedState
+ toState:bookmarks::kShowingState]) {
+ [[self backgroundGradientView] setShowsDivider:YES];
+ [[self view] setHidden:NO];
+ AnimatableView* view = [self animatableView];
+ // Height takes into account the extra height we have since the toolbar
+ // only compresses when we're done.
+ [view animateToNewHeight:(bookmarks::kBookmarkBarHeight -
+ kBookmarkBarOverlap)
+ duration:kBookmarkBarAnimationDuration];
+ } else {
+ // Oops! An animation we don't know how to handle.
+ return NO;
+ }
+
+ return YES;
+}
+
+// Enable or disable items. We are the menu delegate for both the bar
+// and for bookmark folder buttons.
+- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)anItem {
+ // NSUserInterfaceValidations says that the passed-in object has type
+ // |id<NSValidatedUserInterfaceItem>|, but this function needs to call the
+ // NSObject method -isKindOfClass: on the parameter. In theory, this is not
+ // correct, but this is probably a bug in the method signature.
+ NSMenuItem* item = static_cast<NSMenuItem*>(anItem);
+ // Yes for everything we don't explicitly deny.
+ if (![item isKindOfClass:[NSMenuItem class]])
+ return YES;
+
+ // Yes if we're not a special BookmarkMenu.
+ if (![[item menu] isKindOfClass:[BookmarkMenu class]])
+ return YES;
+
+ // No if we think it's a special BookmarkMenu but have trouble.
+ const BookmarkNode* node = [self nodeFromMenuItem:item];
+ if (!node)
+ return NO;
+
+ // If this is the bar menu, we only have things to do if there are
+ // buttons. If this is a folder button menu, we only have things to
+ // do if the folder has items.
+ NSMenu* menu = [item menu];
+ BOOL thingsToDo = NO;
+ if (menu == [[self view] menu]) {
+ thingsToDo = [buttons_ count] ? YES : NO;
+ } else {
+ if (node && node->is_folder() && node->GetChildCount()) {
+ thingsToDo = YES;
+ }
+ }
+
+ // Disable openAll* if we have nothing to do.
+ SEL action = [item action];
+ if ((!thingsToDo) &&
+ ((action == @selector(openAllBookmarks:)) ||
+ (action == @selector(openAllBookmarksNewWindow:)) ||
+ (action == @selector(openAllBookmarksIncognitoWindow:)))) {
+ return NO;
+ }
+
+ if ((action == @selector(editBookmark:)) ||
+ (action == @selector(deleteBookmark:)) ||
+ (action == @selector(cutBookmark:)) ||
+ (action == @selector(copyBookmark:))) {
+ if (![self canEditBookmark:node]) {
+ return NO;
+ }
+ }
+
+ if (action == @selector(pasteBookmark:) &&
+ !bookmark_utils::CanPasteFromClipboard(node))
+ return NO;
+
+ // If this is an incognito window, don't allow "open in incognito".
+ if ((action == @selector(openBookmarkInIncognitoWindow:)) ||
+ (action == @selector(openAllBookmarksIncognitoWindow:))) {
+ if (browser_->profile()->IsOffTheRecord()) {
+ return NO;
+ }
+ }
+
+ // Enabled by default.
+ return YES;
+}
+
+// Actually open the URL. This is the last chance for a unit test to
+// override.
+- (void)openURL:(GURL)url disposition:(WindowOpenDisposition)disposition {
+ browser_->OpenURL(url, GURL(), disposition, PageTransition::AUTO_BOOKMARK);
+}
+
+- (void)clearMenuTagMap {
+ seedId_ = 0;
+ menuTagMap_.clear();
+}
+
+- (int)preferredHeight {
+ DCHECK(![self isAnimationRunning]);
+
+ if (!barIsEnabled_)
+ return 0;
+
+ switch (visualState_) {
+ case bookmarks::kShowingState:
+ return bookmarks::kBookmarkBarHeight;
+ case bookmarks::kDetachedState:
+ return bookmarks::kNTPBookmarkBarHeight;
+ case bookmarks::kHiddenState:
+ return 0;
+ case bookmarks::kInvalidState:
+ default:
+ NOTREACHED();
+ return 0;
+ }
+}
+
+// Recursively add the given bookmark node and all its children to
+// menu, one menu item per node.
+- (void)addNode:(const BookmarkNode*)child toMenu:(NSMenu*)menu {
+ NSString* title = [BookmarkMenuCocoaController menuTitleForNode:child];
+ NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:title
+ action:nil
+ keyEquivalent:@""] autorelease];
+ [menu addItem:item];
+ [item setImage:[self favIconForNode:child]];
+ if (child->is_folder()) {
+ NSMenu* submenu = [[[NSMenu alloc] initWithTitle:title] autorelease];
+ [menu setSubmenu:submenu forItem:item];
+ if (child->GetChildCount()) {
+ [self addFolderNode:child toMenu:submenu]; // potentially recursive
+ } else {
+ [self tagEmptyMenu:submenu];
+ }
+ } else {
+ [item setTarget:self];
+ [item setAction:@selector(openBookmarkMenuItem:)];
+ [item setTag:[self menuTagFromNodeId:child->id()]];
+ // Add a tooltip
+ std::string url_string = child->GetURL().possibly_invalid_spec();
+ NSString* tooltip = [NSString stringWithFormat:@"%@\n%s",
+ base::SysUTF16ToNSString(child->GetTitle()),
+ url_string.c_str()];
+ [item setToolTip:tooltip];
+ }
+}
+
+// Empty menus are odd; if empty, add something to look at.
+// Matches windows behavior.
+- (void)tagEmptyMenu:(NSMenu*)menu {
+ NSString* empty_menu_title = l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU);
+ [menu addItem:[[[NSMenuItem alloc] initWithTitle:empty_menu_title
+ action:NULL
+ keyEquivalent:@""] autorelease]];
+}
+
+// Add the children of the given bookmark node (and their children...)
+// to menu, one menu item per node.
+- (void)addFolderNode:(const BookmarkNode*)node toMenu:(NSMenu*)menu {
+ for (int i = 0; i < node->GetChildCount(); i++) {
+ const BookmarkNode* child = node->GetChild(i);
+ [self addNode:child toMenu:menu];
+ }
+}
+
+// Return an autoreleased NSMenu that represents the given bookmark
+// folder node.
+- (NSMenu *)menuForFolderNode:(const BookmarkNode*)node {
+ if (!node->is_folder())
+ return nil;
+ NSString* title = base::SysUTF16ToNSString(node->GetTitle());
+ NSMenu* menu = [[[NSMenu alloc] initWithTitle:title] autorelease];
+ [self addFolderNode:node toMenu:menu];
+
+ if (![menu numberOfItems]) {
+ [self tagEmptyMenu:menu];
+ }
+ return menu;
+}
+
+// Return an appropriate width for the given bookmark button cell.
+// The "+2" is needed because, sometimes, Cocoa is off by a tad.
+// Example: for a bookmark named "Moma" or "SFGate", it is one pixel
+// too small. For "FBL" it is 2 pixels too small.
+// For a bookmark named "SFGateFooWoo", it is just fine.
+- (CGFloat)widthForBookmarkButtonCell:(NSCell*)cell {
+ CGFloat desired = [cell cellSize].width + 2;
+ return std::min(desired, bookmarks::kDefaultBookmarkWidth);
+}
+
+- (IBAction)openBookmarkMenuItem:(id)sender {
+ int64 tag = [self nodeIdFromMenuTag:[sender tag]];
+ const BookmarkNode* node = bookmarkModel_->GetNodeByID(tag);
+ WindowOpenDisposition disposition =
+ event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
+ [self openURL:node->GetURL() disposition:disposition];
+}
+
+// For the given root node of the bookmark bar, show or hide (as
+// appropriate) the "no items" container (text which says "bookmarks
+// go here").
+- (void)showOrHideNoItemContainerForNode:(const BookmarkNode*)node {
+ BOOL hideNoItemWarning = node->GetChildCount() > 0;
+ [[buttonView_ noItemContainer] setHidden:hideNoItemWarning];
+}
+
+// TODO(jrg): write a "build bar" so there is a nice spot for things
+// like the contextual menu which is invoked when not over a
+// bookmark. On Safari that menu has a "new folder" option.
+- (void)addNodesToButtonList:(const BookmarkNode*)node {
+ [self showOrHideNoItemContainerForNode:node];
+
+ CGFloat maxViewX = NSMaxX([[self view] bounds]);
+ int xOffset = 0;
+ for (int i = 0; i < node->GetChildCount(); i++) {
+ const BookmarkNode* child = node->GetChild(i);
+ BookmarkButton* button = [self buttonForNode:child xOffset:&xOffset];
+ if (NSMinX([button frame]) >= maxViewX)
+ break;
+ [buttons_ addObject:button];
+ }
+}
+
+- (BookmarkButton*)buttonForNode:(const BookmarkNode*)node
+ xOffset:(int*)xOffset {
+ BookmarkButtonCell* cell = [self cellForBookmarkNode:node];
+ NSRect frame = [self frameForBookmarkButtonFromCell:cell xOffset:xOffset];
+
+ scoped_nsobject<BookmarkButton>
+ button([[BookmarkButton alloc] initWithFrame:frame]);
+ DCHECK(button.get());
+
+ // [NSButton setCell:] warns to NOT use setCell: other than in the
+ // initializer of a control. However, we are using a basic
+ // NSButton whose initializer does not take an NSCell as an
+ // object. To honor the assumed semantics, we do nothing with
+ // NSButton between alloc/init and setCell:.
+ [button setCell:cell];
+ [button setDelegate:self];
+
+ // We cannot set the button cell's text color until it is placed in
+ // the button (e.g. the [button setCell:cell] call right above). We
+ // also cannot set the cell's text color until the view is added to
+ // the hierarchy. If that second part is now true, set the color.
+ // (If not we'll set the color on the 1st themeChanged:
+ // notification.)
+ ThemeProvider* themeProvider = [[[self view] window] themeProvider];
+ if (themeProvider) {
+ NSColor* color =
+ themeProvider->GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT,
+ true);
+ [cell setTextColor:color];
+ }
+
+ 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::SysUTF16ToNSString(node->GetTitle());
+ std::string url_string = node->GetURL().possibly_invalid_spec();
+ NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", title,
+ url_string.c_str()];
+ [button setToolTip:tooltip];
+ }
+ return [[button.get() retain] autorelease];
+}
+
+// Add non-bookmark buttons to the view. This includes the chevron
+// and the "other bookmarks" button. Technically "other bookmarks" is
+// a bookmark button but it is treated specially. Only needs to be
+// called when these buttons are new or when the bookmark bar is
+// cleared (e.g. on a loaded: call). Unlike addButtonsToView below,
+// we don't need to add/remove these dynamically in response to window
+// resize.
+- (void)addNonBookmarkButtonsToView {
+ [buttonView_ addSubview:otherBookmarksButton_.get()];
+ [buttonView_ addSubview:offTheSideButton_];
+}
+
+// Add bookmark buttons to the view only if they are completely
+// visible and don't overlap the "other bookmarks". Remove buttons
+// which are clipped. Called when building the bookmark bar the first time.
+- (void)addButtonsToView {
+ displayedButtonCount_ = 0;
+ NSMutableArray* buttons = [self buttons];
+ for (NSButton* button in buttons) {
+ if (NSMaxX([button frame]) > (NSMinX([offTheSideButton_ frame]) -
+ bookmarks::kBookmarkHorizontalPadding))
+ break;
+ [buttonView_ addSubview:button];
+ ++displayedButtonCount_;
+ }
+ NSUInteger removalCount =
+ [buttons count] - (NSUInteger)displayedButtonCount_;
+ if (removalCount > 0) {
+ NSRange removalRange = NSMakeRange(displayedButtonCount_, removalCount);
+ [buttons removeObjectsInRange:removalRange];
+ }
+}
+
+// Create the button for "Other Bookmarks" on the right of the bar.
+- (void)createOtherBookmarksButton {
+ // Can't create this until the model is loaded, but only need to
+ // create it once.
+ if (otherBookmarksButton_.get())
+ return;
+
+ // TODO(jrg): remove duplicate code
+ NSCell* cell = [self cellForBookmarkNode:bookmarkModel_->other_node()];
+ int ignored = 0;
+ NSRect frame = [self frameForBookmarkButtonFromCell:cell xOffset:&ignored];
+ frame.origin.x = [[self buttonView] bounds].size.width - frame.size.width;
+ frame.origin.x -= bookmarks::kBookmarkHorizontalPadding;
+ BookmarkButton* button = [[BookmarkButton alloc] initWithFrame:frame];
+ [button setDraggable:NO];
+ otherBookmarksButton_.reset(button);
+ view_id_util::SetID(button, VIEW_ID_OTHER_BOOKMARKS);
+
+ // Make sure this button, like all other BookmarkButtons, lives
+ // until the end of the current event loop.
+ [[button retain] autorelease];
+
+ // Peg at right; keep same height as bar.
+ [button setAutoresizingMask:(NSViewMinXMargin)];
+ [button setCell:cell];
+ [button setDelegate:self];
+ [button setTarget:self];
+ [button setAction:@selector(openBookmarkFolderFromButton:)];
+ [buttonView_ addSubview:button];
+
+ // Now that it's here, move the chevron over.
+ [self positionOffTheSideButton];
+}
+
+// Now that the model is loaded, set the bookmark bar root as the node
+// represented by the bookmark bar (default, background) menu.
+- (void)setNodeForBarMenu {
+ const BookmarkNode* node = bookmarkModel_->GetBookmarkBarNode();
+ BookmarkMenu* menu = static_cast<BookmarkMenu*>([[self view] menu]);
+
+ // Make sure types are compatible
+ DCHECK(sizeof(long long) == sizeof(int64));
+ [menu setRepresentedObject:[NSNumber numberWithLongLong:node->id()]];
+}
+
+// To avoid problems with sync, changes that may impact the current
+// bookmark (e.g. deletion) make sure context menus are closed. This
+// prevents deleting a node which no longer exists.
+- (void)cancelMenuTracking {
+ [buttonContextMenu_ cancelTracking];
+ [buttonFolderContextMenu_ cancelTracking];
+}
+
+// Determines the appropriate state for the given situation.
++ (bookmarks::VisualState)visualStateToShowNormalBar:(BOOL)showNormalBar
+ showDetachedBar:(BOOL)showDetachedBar {
+ if (showNormalBar)
+ return bookmarks::kShowingState;
+ if (showDetachedBar)
+ return bookmarks::kDetachedState;
+ return bookmarks::kHiddenState;
+}
+
+- (void)moveToVisualState:(bookmarks::VisualState)nextVisualState
+ withAnimation:(BOOL)animate {
+ BOOL isAnimationRunning = [self isAnimationRunning];
+
+ // No-op if the next state is the same as the "current" one, subject to the
+ // following conditions:
+ // - no animation is running; or
+ // - an animation is running and |animate| is YES ([*] if it's NO, we'd want
+ // to cancel the animation and jump to the final state).
+ if ((nextVisualState == visualState_) && (!isAnimationRunning || animate))
+ return;
+
+ // If an animation is running, we want to finalize it. Otherwise we'd have to
+ // be able to animate starting from the middle of one type of animation. We
+ // assume that animations that we know about can be "reversed".
+ if (isAnimationRunning) {
+ // Don't cancel if we're going to reverse the animation.
+ if (nextVisualState != lastVisualState_) {
+ [self stopCurrentAnimation];
+ [self finalizeVisualState];
+ }
+
+ // If we're in case [*] above, we can stop here.
+ if (nextVisualState == visualState_)
+ return;
+ }
+
+ // Now update with the new state change.
+ lastVisualState_ = visualState_;
+ visualState_ = nextVisualState;
+
+ // Animate only if told to and if bar is enabled.
+ if (animate && !ignoreAnimations_ && barIsEnabled_) {
+ [self closeAllBookmarkFolders];
+ // Take care of any animation cases we know how to handle.
+
+ // We know how to handle hidden <-> normal, normal <-> detached....
+ if ([self isAnimatingBetweenState:bookmarks::kHiddenState
+ andState:bookmarks::kShowingState] ||
+ [self isAnimatingBetweenState:bookmarks::kShowingState
+ andState:bookmarks::kDetachedState]) {
+ [delegate_ bookmarkBar:self willAnimateFromState:lastVisualState_
+ toState:visualState_];
+ [self showBookmarkBarWithAnimation:YES];
+ return;
+ }
+
+ // If we ever need any other animation cases, code would go here.
+ // Let any animation cases which we don't know how to handle fall through to
+ // the unanimated case.
+ }
+
+ // Just jump to the state.
+ [self finalizeVisualState];
+}
+
+// N.B.: |-moveToVisualState:...| will check if this should be a no-op or not.
+- (void)updateAndShowNormalBar:(BOOL)showNormalBar
+ showDetachedBar:(BOOL)showDetachedBar
+ withAnimation:(BOOL)animate {
+ bookmarks::VisualState newVisualState =
+ [BookmarkBarController visualStateToShowNormalBar:showNormalBar
+ showDetachedBar:showDetachedBar];
+ [self moveToVisualState:newVisualState
+ withAnimation:animate && !ignoreAnimations_];
+}
+
+// (Private)
+- (void)finalizeVisualState {
+ // We promise that our delegate that the variables will be finalized before
+ // the call to |-bookmarkBar:didChangeFromState:toState:|.
+ bookmarks::VisualState oldVisualState = lastVisualState_;
+ lastVisualState_ = bookmarks::kInvalidState;
+
+ // Notify our delegate.
+ [delegate_ bookmarkBar:self didChangeFromState:oldVisualState
+ toState:visualState_];
+
+ // Update ourselves visually.
+ [self updateVisibility];
+}
+
+// (Private)
+- (void)stopCurrentAnimation {
+ [[self animatableView] stopAnimation];
+}
+
+// Delegate method for |AnimatableView| (a superclass of
+// |BookmarkBarToolbarView|).
+- (void)animationDidEnd:(NSAnimation*)animation {
+ [self finalizeVisualState];
+}
+
+- (void)reconfigureBookmarkBar {
+ [self redistributeButtonsOnBarAsNeeded];
+ [self positionOffTheSideButton];
+ [self configureOffTheSideButtonContentsAndVisibility];
+ [self centerNoItemsLabel];
+}
+
+// Determine if the given |view| can completely fit within the constraint of
+// maximum x, given by |maxViewX|, and, if not, narrow the view up to a minimum
+// width. If the minimum width is not achievable then hide the view. Return YES
+// if the view was hidden.
+- (BOOL)shrinkOrHideView:(NSView*)view forMaxX:(CGFloat)maxViewX {
+ BOOL wasHidden = NO;
+ // See if the view needs to be narrowed.
+ NSRect frame = [view frame];
+ if (NSMaxX(frame) > maxViewX) {
+ // Resize if more than 30 pixels are showing, otherwise hide.
+ if (NSMinX(frame) + 30.0 < maxViewX) {
+ frame.size.width = maxViewX - NSMinX(frame);
+ [view setFrame:frame];
+ } else {
+ [view setHidden:YES];
+ wasHidden = YES;
+ }
+ }
+ return wasHidden;
+}
+
+// Adjust the horizontal width and the visibility of the "For quick access"
+// text field and "Import bookmarks..." button based on the current width
+// of the containing |buttonView_| (which is affected by window width).
+- (void)adjustNoItemContainerWidthsForMaxX:(CGFloat)maxViewX {
+ if (![[buttonView_ noItemContainer] isHidden]) {
+ // Reset initial frames for the two items, then adjust as necessary.
+ NSTextField* noItemTextfield = [buttonView_ noItemTextfield];
+ [noItemTextfield setFrame:originalNoItemsRect_];
+ [noItemTextfield setHidden:NO];
+ NSButton* importBookmarksButton = [buttonView_ importBookmarksButton];
+ [importBookmarksButton setFrame:originalImportBookmarksRect_];
+ [importBookmarksButton setHidden:NO];
+ // Check each to see if they need to be shrunk or hidden.
+ if ([self shrinkOrHideView:importBookmarksButton forMaxX:maxViewX])
+ [self shrinkOrHideView:noItemTextfield forMaxX:maxViewX];
+ }
+}
+
+- (void)redistributeButtonsOnBarAsNeeded {
+ const BookmarkNode* node = bookmarkModel_->GetBookmarkBarNode();
+ NSInteger barCount = node->GetChildCount();
+
+ // Determine the current maximum extent of the visible buttons.
+ CGFloat maxViewX = NSMaxX([[self view] bounds]);
+ NSButton* otherBookmarksButton = otherBookmarksButton_.get();
+ // If necessary, pull in the width to account for the Other Bookmarks button.
+ if (otherBookmarksButton_)
+ maxViewX = [otherBookmarksButton frame].origin.x -
+ bookmarks::kBookmarkHorizontalPadding;
+ // If we're already overflowing, then we need to account for the chevron.
+ if (barCount > displayedButtonCount_)
+ maxViewX = [offTheSideButton_ frame].origin.x -
+ bookmarks::kBookmarkHorizontalPadding;
+
+ // As a result of pasting or dragging, the bar may now have more buttons
+ // than will fit so remove any which overflow. They will be shown in
+ // the off-the-side folder.
+ while (displayedButtonCount_ > 0) {
+ BookmarkButton* button = [buttons_ lastObject];
+ if (NSMaxX([button frame]) < maxViewX)
+ break;
+ [buttons_ removeLastObject];
+ [button setDelegate:nil];
+ [button removeFromSuperview];
+ --displayedButtonCount_;
+ }
+
+ // As a result of cutting, deleting and dragging, the bar may now have room
+ // for more buttons.
+ int xOffset = displayedButtonCount_ > 0 ?
+ NSMaxX([[buttons_ lastObject] frame]) +
+ bookmarks::kBookmarkHorizontalPadding : 0;
+ for (int i = displayedButtonCount_; i < barCount; ++i) {
+ const BookmarkNode* child = node->GetChild(i);
+ BookmarkButton* button = [self buttonForNode:child xOffset:&xOffset];
+ // If we're testing against the last possible button then account
+ // for the chevron no longer needing to be shown.
+ if (i == barCount + 1)
+ maxViewX += NSWidth([offTheSideButton_ frame]) +
+ bookmarks::kBookmarkHorizontalPadding;
+ if (NSMaxX([button frame]) >= maxViewX)
+ break;
+ ++displayedButtonCount_;
+ [buttons_ addObject:button];
+ [buttonView_ addSubview:button];
+ }
+
+ // While we're here, adjust the horizontal width and the visibility
+ // of the "For quick access" and "Import bookmarks..." text fields.
+ if (![buttons_ count])
+ [self adjustNoItemContainerWidthsForMaxX:maxViewX];
+}
+
+#pragma mark Private Methods Exposed for Testing
+
+- (BookmarkBarView*)buttonView {
+ return buttonView_;
+}
+
+- (NSMutableArray*)buttons {
+ return buttons_.get();
+}
+
+- (NSButton*)offTheSideButton {
+ return offTheSideButton_;
+}
+
+- (BOOL)offTheSideButtonIsHidden {
+ return [offTheSideButton_ isHidden];
+}
+
+- (BookmarkButton*)otherBookmarksButton {
+ return otherBookmarksButton_.get();
+}
+
+- (BookmarkBarFolderController*)folderController {
+ return folderController_;
+}
+
+- (id)folderTarget {
+ return folderTarget_.get();
+}
+
+- (int)displayedButtonCount {
+ return displayedButtonCount_;
+}
+
+// Delete all buttons (bookmarks, chevron, "other bookmarks") from the
+// bookmark bar; reset knowledge of bookmarks.
+- (void)clearBookmarkBar {
+ for (BookmarkButton* button in buttons_.get()) {
+ [button setDelegate:nil];
+ [button removeFromSuperview];
+ }
+ [buttons_ removeAllObjects];
+ [self clearMenuTagMap];
+ displayedButtonCount_ = 0;
+
+ // Make sure there are no stale pointers in the pasteboard. This
+ // can be important if a bookmark is deleted (via bookmark sync)
+ // while in the middle of a drag. The "drag completed" code
+ // (e.g. [BookmarkBarView performDragOperationForBookmarkButton:]) is
+ // careful enough to bail if there is no data found at "drop" time.
+ //
+ // Unfortunately the clearContents selector is 10.6 only. The best
+ // we can do is make sure something else is present in place of the
+ // stale bookmark.
+ NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
+ [pboard declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:self];
+ [pboard setString:@"" forType:NSStringPboardType];
+}
+
+// Return an autoreleased NSCell suitable for a bookmark button.
+// TODO(jrg): move much of the cell config into the BookmarkButtonCell class.
+- (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)node {
+ NSImage* image = node ? [self favIconForNode:node] : nil;
+ NSMenu* menu = node && node->is_folder() ? buttonFolderContextMenu_ :
+ buttonContextMenu_;
+ BookmarkButtonCell* cell = [BookmarkButtonCell buttonCellForNode:node
+ contextMenu:menu
+ cellText:nil
+ cellImage:image];
+ [cell setTag:kStandardButtonTypeWithLimitedClickFeedback];
+
+ // 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.
+
+ return cell;
+}
+
+// Returns a frame appropriate for the given bookmark cell, suitable
+// for creating an NSButton that will contain it. |xOffset| is the X
+// offset for the frame; it is increased to be an appropriate X offset
+// for the next button.
+- (NSRect)frameForBookmarkButtonFromCell:(NSCell*)cell
+ xOffset:(int*)xOffset {
+ DCHECK(xOffset);
+ NSRect bounds = [buttonView_ bounds];
+ bounds.size.height = bookmarks::kBookmarkButtonHeight;
+
+ NSRect frame = NSInsetRect(bounds,
+ bookmarks::kBookmarkHorizontalPadding,
+ bookmarks::kBookmarkVerticalPadding);
+ frame.size.width = [self widthForBookmarkButtonCell:cell];
+
+ // Add an X offset based on what we've already done
+ frame.origin.x += *xOffset;
+
+ // And up the X offset for next time.
+ *xOffset = NSMaxX(frame);
+
+ return frame;
+}
+
+// A bookmark button's contents changed. Check for growth
+// (e.g. increase the width up to the maximum). If we grew, move
+// other bookmark buttons over.
+- (void)checkForBookmarkButtonGrowth:(NSButton*)button {
+ NSRect frame = [button frame];
+ CGFloat desiredSize = [self widthForBookmarkButtonCell:[button cell]];
+ CGFloat delta = desiredSize - frame.size.width;
+ if (delta) {
+ frame.size.width = desiredSize;
+ [button setFrame:frame];
+ for (NSButton* button in buttons_.get()) {
+ NSRect buttonFrame = [button frame];
+ if (buttonFrame.origin.x > frame.origin.x) {
+ buttonFrame.origin.x += delta;
+ [button setFrame:buttonFrame];
+ }
+ }
+ }
+ // We may have just crossed a threshold to enable the off-the-side
+ // button.
+ [self configureOffTheSideButtonContentsAndVisibility];
+}
+
+// Called when our controlled frame has changed size.
+- (void)frameDidChange {
+ if (!bookmarkModel_->IsLoaded())
+ return;
+ [self updateTheme:[[[self view] window] themeProvider]];
+ [self reconfigureBookmarkBar];
+}
+
+// Given a NSMenuItem tag, return the appropriate bookmark node id.
+- (int64)nodeIdFromMenuTag:(int32)tag {
+ return menuTagMap_[tag];
+}
+
+// Create and return a new tag for the given node id.
+- (int32)menuTagFromNodeId:(int64)menuid {
+ int tag = seedId_++;
+ menuTagMap_[tag] = menuid;
+ return tag;
+}
+
+// Return the BookmarkNode associated with the given NSMenuItem. Can
+// return NULL which means "do nothing". One case where it would
+// return NULL is if the bookmark model gets modified while you have a
+// context menu open.
+- (const BookmarkNode*)nodeFromMenuItem:(id)sender {
+ const BookmarkNode* node = NULL;
+ BookmarkMenu* menu = (BookmarkMenu*)[sender menu];
+ if ([menu isKindOfClass:[BookmarkMenu class]]) {
+ int64 id = [menu id];
+ node = bookmarkModel_->GetNodeByID(id);
+ }
+ return node;
+}
+
+// Adapt appearance of buttons to the current theme. Called after
+// theme changes, or when our view is added to the view hierarchy.
+// Oddly, the view pings us instead of us pinging our view. This is
+// because our trigger is an [NSView viewWillMoveToWindow:], which the
+// controller doesn't normally know about. Otherwise we don't have
+// access to the theme before we know what window we will be on.
+- (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];
+ }
+ [[otherBookmarksButton_ cell] setTextColor:color];
+}
+
+// Return YES if the event indicates an exit from the bookmark bar
+// folder menus. E.g. "click outside" of the area we are watching.
+// At this time we are watching the area that includes all popup
+// bookmark folder windows.
+- (BOOL)isEventAnExitEvent:(NSEvent*)event {
+ NSWindow* eventWindow = [event window];
+ NSWindow* myWindow = [[self view] window];
+ switch ([event type]) {
+ case NSLeftMouseDown:
+ case NSRightMouseDown:
+ // If the click is in my window but NOT in the bookmark bar, consider
+ // it a click 'outside'. Clicks directly on an active button (i.e. one
+ // that is a folder and for which its folder menu is showing) are 'in'.
+ // All other clicks on the bookmarks bar are counted as 'outside'
+ // because they should close any open bookmark folder menu.
+ if (eventWindow == myWindow) {
+ NSView* hitView =
+ [[eventWindow contentView] hitTest:[event locationInWindow]];
+ if (hitView == [folderController_ parentButton])
+ return NO;
+ if (![hitView isDescendantOf:[self view]] || hitView == buttonView_)
+ 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]]) {
+ return YES;
+ }
+ break;
+ case NSKeyDown:
+ case NSKeyUp:
+ // Any key press ends things.
+ return YES;
+ case NSLeftMouseDragged:
+ // We can get here with the following sequence:
+ // - open a bookmark folder
+ // - right-click (and unclick) on it to open context menu
+ // - move mouse to window titlebar then click-drag it by the titlebar
+ // http://crbug.com/49333
+ return YES;
+ default:
+ break;
+ }
+ return NO;
+}
+
+#pragma mark Drag & Drop
+
+// 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) || (![button superview]))
+ 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 {
+ point = [[self view] convertPoint:point
+ fromView:[[[self view] window] contentView]];
+ BookmarkButton* button = [self buttonForDroppingOnAtPoint:point
+ fromArray:buttons_.get()];
+ // One more chance -- try "Other Bookmarks" and "off the side" (if visible).
+ // This is different than BookmarkBarFolderController.
+ if (!button) {
+ NSMutableArray* array = [NSMutableArray array];
+ if (![self offTheSideButtonIsHidden])
+ [array addObject:offTheSideButton_];
+ [array addObject:otherBookmarksButton_];
+ button = [self buttonForDroppingOnAtPoint:point
+ fromArray:array];
+ }
+ return button;
+}
+
+- (int)indexForDragToPoint:(NSPoint)point {
+ // TODO(jrg): revisit position info based on UI team feedback.
+ // dropLocation is in bar local coordinates.
+ NSPoint dropLocation =
+ [[self view] convertPoint:point
+ fromView:[[[self view] window] contentView]];
+ BookmarkButton* buttonToTheRightOfDraggedButton = nil;
+ for (BookmarkButton* button in buttons_.get()) {
+ CGFloat midpoint = NSMidX([button frame]);
+ if (dropLocation.x <= midpoint) {
+ buttonToTheRightOfDraggedButton = button;
+ break;
+ }
+ }
+ if (buttonToTheRightOfDraggedButton) {
+ const BookmarkNode* afterNode =
+ [buttonToTheRightOfDraggedButton bookmarkNode];
+ DCHECK(afterNode);
+ int index = afterNode->GetParent()->IndexOfChild(afterNode);
+ // Make sure we don't get confused by buttons which aren't visible.
+ return std::min(index, displayedButtonCount_);
+ }
+
+ // If nothing is to my right I am at the end!
+ return displayedButtonCount_;
+}
+
+// TODO(mrossetti,jrg): Yet more duplicated code.
+// http://crbug.com/35966
+- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
+ to:(NSPoint)point
+ copy:(BOOL)copy {
+ DCHECK(sourceNode);
+ // Drop destination.
+ const BookmarkNode* destParent = NULL;
+ int destIndex = 0;
+
+ // First check if we're dropping on a button. If we have one, and
+ // it's a folder, drop in it.
+ BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
+ if ([button isFolder]) {
+ destParent = [button bookmarkNode];
+ // Drop it at the end.
+ destIndex = [button bookmarkNode]->GetChildCount();
+ } else {
+ // Else we're dropping somewhere on the bar, so find the right spot.
+ destParent = bookmarkModel_->GetBookmarkBarNode();
+ destIndex = [self indexForDragToPoint:point];
+ }
+
+ // Be sure we don't try and drop a folder into itself.
+ if (sourceNode != destParent) {
+ if (copy)
+ bookmarkModel_->Copy(sourceNode, destParent, destIndex);
+ else
+ bookmarkModel_->Move(sourceNode, destParent, destIndex);
+ }
+
+ [self closeFolderAndStopTrackingMenus];
+
+ // Movement of a node triggers observers (like us) to rebuild the
+ // bar so we don't have to do so explicitly.
+
+ return YES;
+}
+
+- (void)draggingEnded:(id<NSDraggingInfo>)info {
+ [self closeFolderAndStopTrackingMenus];
+}
+
+#pragma mark Bridge Notification Handlers
+
+// TODO(jrg): for now this is brute force.
+- (void)loaded:(BookmarkModel*)model {
+ DCHECK(model == bookmarkModel_);
+ if (!model->IsLoaded())
+ return;
+
+ // If this is a rebuild request while we have a folder open, close it.
+ // TODO(mrossetti): Eliminate the need for this because it causes the folder
+ // menu to disappear after a cut/copy/paste/delete change.
+ // See: http://crbug.com/36614
+ if (folderController_)
+ [self closeAllBookmarkFolders];
+
+ // Brute force nuke and build.
+ savedFrameWidth_ = NSWidth([[self view] frame]);
+ const BookmarkNode* node = model->GetBookmarkBarNode();
+ [self clearBookmarkBar];
+ [self addNodesToButtonList:node];
+ [self createOtherBookmarksButton];
+ [self updateTheme:[[[self view] window] themeProvider]];
+ [self positionOffTheSideButton];
+ [self addNonBookmarkButtonsToView];
+ [self addButtonsToView];
+ [self configureOffTheSideButtonContentsAndVisibility];
+ [self setNodeForBarMenu];
+}
+
+- (void)beingDeleted:(BookmarkModel*)model {
+ // The browser may be being torn down; little is safe to do. As an
+ // example, it may not be safe to clear the pasteboard.
+ // http://crbug.com/38665
+}
+
+- (void)nodeAdded:(BookmarkModel*)model
+ parent:(const BookmarkNode*)newParent index:(int)newIndex {
+ // If a context menu is open, close it.
+ [self cancelMenuTracking];
+
+ const BookmarkNode* newNode = newParent->GetChild(newIndex);
+ id<BookmarkButtonControllerProtocol> newController =
+ [self controllerForNode:newParent];
+ [newController addButtonForNode:newNode atIndex:newIndex];
+ // If we go from 0 --> 1 bookmarks we may need to hide the
+ // "bookmarks go here" text container.
+ [self showOrHideNoItemContainerForNode:model->GetBookmarkBarNode()];
+}
+
+// TODO(jrg): for now this is brute force.
+- (void)nodeChanged:(BookmarkModel*)model
+ node:(const BookmarkNode*)node {
+ [self loaded:model];
+}
+
+- (void)nodeMoved:(BookmarkModel*)model
+ oldParent:(const BookmarkNode*)oldParent oldIndex:(int)oldIndex
+ newParent:(const BookmarkNode*)newParent newIndex:(int)newIndex {
+ const BookmarkNode* movedNode = newParent->GetChild(newIndex);
+ id<BookmarkButtonControllerProtocol> oldController =
+ [self controllerForNode:oldParent];
+ id<BookmarkButtonControllerProtocol> newController =
+ [self controllerForNode:newParent];
+ if (newController == oldController) {
+ [oldController moveButtonFromIndex:oldIndex toIndex:newIndex];
+ } else {
+ [oldController removeButton:oldIndex animate:NO];
+ [newController addButtonForNode:movedNode atIndex:newIndex];
+ }
+ // If the bar is one of the parents we may need to update the visibility
+ // of the "bookmarks go here" presentation.
+ [self showOrHideNoItemContainerForNode:model->GetBookmarkBarNode()];
+ // If we moved the only item on the "off the side" menu somewhere
+ // else, we may no longer need to show it.
+ [self configureOffTheSideButtonContentsAndVisibility];
+}
+
+- (void)nodeRemoved:(BookmarkModel*)model
+ parent:(const BookmarkNode*)oldParent index:(int)index {
+ // If a context menu is open, close it.
+ [self cancelMenuTracking];
+
+ // Locate the parent node. The parent may not be showing, in which case
+ // we do nothing.
+ id<BookmarkButtonControllerProtocol> parentController =
+ [self controllerForNode:oldParent];
+ [parentController removeButton:index animate:YES];
+ // If we go from 1 --> 0 bookmarks we may need to show the
+ // "bookmarks go here" text container.
+ [self showOrHideNoItemContainerForNode:model->GetBookmarkBarNode()];
+ // If we deleted the only item on the "off the side" menu we no
+ // longer need to show it.
+ [self configureOffTheSideButtonContentsAndVisibility];
+}
+
+// TODO(jrg): linear searching is bad.
+// Need a BookmarkNode-->NSCell mapping.
+//
+// TODO(jrg): if the bookmark bar is open on launch, we see the
+// buttons all placed, then "scooted over" as the favicons load. If
+// this looks bad I may need to change widthForBookmarkButtonCell to
+// add space for an image even if not there on the assumption that
+// favicons will eventually load.
+- (void)nodeFavIconLoaded:(BookmarkModel*)model
+ node:(const BookmarkNode*)node {
+ for (BookmarkButton* button in buttons_.get()) {
+ const BookmarkNode* cellnode = [button bookmarkNode];
+ if (cellnode == node) {
+ [[button cell] setBookmarkCellText:[button title]
+ image:[self favIconForNode: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.
+ [self checkForBookmarkButtonGrowth:button];
+ }
+ }
+}
+
+// TODO(jrg): for now this is brute force.
+- (void)nodeChildrenReordered:(BookmarkModel*)model
+ node:(const BookmarkNode*)node {
+ [self loaded:model];
+}
+
+#pragma mark BookmarkBarState Protocol
+
+// (BookmarkBarState protocol)
+- (BOOL)isVisible {
+ return barIsEnabled_ && (visualState_ == bookmarks::kShowingState ||
+ visualState_ == bookmarks::kDetachedState ||
+ lastVisualState_ == bookmarks::kShowingState ||
+ lastVisualState_ == bookmarks::kDetachedState);
+}
+
+// (BookmarkBarState protocol)
+- (BOOL)isAnimationRunning {
+ return lastVisualState_ != bookmarks::kInvalidState;
+}
+
+// (BookmarkBarState protocol)
+- (BOOL)isInState:(bookmarks::VisualState)state {
+ return visualState_ == state &&
+ lastVisualState_ == bookmarks::kInvalidState;
+}
+
+// (BookmarkBarState protocol)
+- (BOOL)isAnimatingToState:(bookmarks::VisualState)state {
+ return visualState_ == state &&
+ lastVisualState_ != bookmarks::kInvalidState;
+}
+
+// (BookmarkBarState protocol)
+- (BOOL)isAnimatingFromState:(bookmarks::VisualState)state {
+ return lastVisualState_ == state;
+}
+
+// (BookmarkBarState protocol)
+- (BOOL)isAnimatingFromState:(bookmarks::VisualState)fromState
+ toState:(bookmarks::VisualState)toState {
+ return lastVisualState_ == fromState && visualState_ == toState;
+}
+
+// (BookmarkBarState protocol)
+- (BOOL)isAnimatingBetweenState:(bookmarks::VisualState)fromState
+ andState:(bookmarks::VisualState)toState {
+ return (lastVisualState_ == fromState && visualState_ == toState) ||
+ (visualState_ == fromState && lastVisualState_ == toState);
+}
+
+// (BookmarkBarState protocol)
+- (CGFloat)detachedMorphProgress {
+ if ([self isInState:bookmarks::kDetachedState]) {
+ return 1;
+ }
+ if ([self isAnimatingToState:bookmarks::kDetachedState]) {
+ return static_cast<CGFloat>(
+ [[self animatableView] currentAnimationProgress]);
+ }
+ if ([self isAnimatingFromState:bookmarks::kDetachedState]) {
+ return static_cast<CGFloat>(
+ 1 - [[self animatableView] currentAnimationProgress]);
+ }
+ return 0;
+}
+
+#pragma mark BookmarkBarToolbarViewController Protocol
+
+- (int)currentTabContentsHeight {
+ TabContents* tc = browser_->GetSelectedTabContents();
+ return tc ? tc->view()->GetContainerSize().height() : 0;
+}
+
+- (ThemeProvider*)themeProvider {
+ return browser_->profile()->GetThemeProvider();
+}
+
+#pragma mark BookmarkButtonDelegate Protocol
+
+- (void)fillPasteboard:(NSPasteboard*)pboard
+ forDragOfButton:(BookmarkButton*)button {
+ [[self folderTarget] fillPasteboard:pboard forDragOfButton:button];
+}
+
+// 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 folder menus are not being shown, do nothing. This is different from
+ // BookmarkBarFolderController's implementation because the bar should NOT
+ // automatically open folder menus when the mouse passes over a folder
+ // button while the BookmarkBarFolderController DOES automically open
+ // a subfolder menu.
+ if (!showFolderMenus_)
+ 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()) {
+ [folderTarget_ openBookmarkFolderFromButton:sender];
+ } else {
+ // We're over a non-folder bookmark so close any old folders.
+ [folderController_ close];
+ folderController_ = nil;
+ }
+}
+
+// BookmarkButtonDelegate protocol implementation.
+- (void)mouseExitedButton:(id)sender event:(NSEvent*)event {
+ // Don't care; do nothing.
+ // This is different behavior that the folder menus.
+}
+
+- (NSWindow*)browserWindow {
+ return [[self view] window];
+}
+
+- (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button {
+ return [self canEditBookmark:[button bookmarkNode]];
+}
+
+- (void)didDragBookmarkToTrash:(BookmarkButton*)button {
+ // TODO(mrossetti): Refactor BookmarkBarFolder common code.
+ // http://crbug.com/35966
+ const BookmarkNode* node = [button bookmarkNode];
+ if (node) {
+ const BookmarkNode* parent = node->GetParent();
+ bookmarkModel_->Remove(parent,
+ parent->IndexOfChild(node));
+ }
+}
+
+#pragma mark BookmarkButtonControllerProtocol
+
+// Close all bookmark folders. "Folder" here is the fake menu for
+// bookmark folders, not a button context menu.
+- (void)closeAllBookmarkFolders {
+ [self watchForExitEvent:NO];
+ [folderController_ 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_;
+}
+
+// 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];
+
+ // Don't allow drops that would result in cycles.
+ if (button) {
+ NSData* data = [[info draggingPasteboard]
+ dataForType:kBookmarkButtonDragType];
+ if (data && [info draggingSource]) {
+ BookmarkButton* sourceButton = nil;
+ [data getBytes:&sourceButton length:sizeof(sourceButton)];
+ const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
+ const BookmarkNode* destNode = [button bookmarkNode];
+ if (destNode->HasAncestor(sourceNode))
+ button = nil;
+ }
+ }
+
+ 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]);
+ DCHECK([[hoverButton_ target]
+ respondsToSelector:@selector(openBookmarkFolderFromButton:)]);
+ [[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();
+ }
+}
+
+- (BOOL)dragShouldLockBarVisibility {
+ return ![self isInState:bookmarks::kDetachedState] &&
+ ![self isAnimatingToState:bookmarks::kDetachedState];
+}
+
+// TODO(mrossetti,jrg): Yet more code dup with BookmarkBarFolderController.
+// http://crbug.com/35966
+- (BOOL)dragButton:(BookmarkButton*)sourceButton
+ to:(NSPoint)point
+ copy:(BOOL)copy {
+ DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]);
+ const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
+ return [self dragBookmark:sourceNode to:point copy:copy];
+}
+
+- (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info {
+ BOOL dragged = NO;
+ std::vector<const BookmarkNode*> nodes([self retrieveBookmarkNodeData]);
+ if (nodes.size()) {
+ BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove);
+ NSPoint dropPoint = [info draggingLocation];
+ for (std::vector<const BookmarkNode*>::const_iterator it = nodes.begin();
+ it != nodes.end(); ++it) {
+ const BookmarkNode* sourceNode = *it;
+ dragged = [self dragBookmark:sourceNode to:dropPoint copy:copy];
+ }
+ }
+ return dragged;
+}
+
+- (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData {
+ std::vector<const BookmarkNode*> dragDataNodes;
+ BookmarkNodeData dragData;
+ if(dragData.ReadFromDragClipboard()) {
+ BookmarkModel* bookmarkModel = [self bookmarkModel];
+ Profile* profile = bookmarkModel->profile();
+ std::vector<const BookmarkNode*> nodes(dragData.GetNodes(profile));
+ dragDataNodes.assign(nodes.begin(), nodes.end());
+ }
+ return dragDataNodes;
+}
+
+// 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)indicatorPosForDragToPoint:(NSPoint)point {
+ CGFloat x = 0;
+ int destIndex = [self indexForDragToPoint:point];
+ int numButtons = displayedButtonCount_;
+
+ // If it's a drop strictly between existing buttons ...
+ 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];
+ x = buttonFrame.origin.x - 0.5 * bookmarks::kBookmarkHorizontalPadding;
+
+ // 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 to its right.
+ BookmarkButton* button =
+ [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex - 1)];
+ DCHECK(button);
+ NSRect buttonFrame = [button frame];
+ x = NSMaxX(buttonFrame) + 0.5 * bookmarks::kBookmarkHorizontalPadding;
+
+ // Otherwise, put it right at the beginning.
+ } else {
+ x = 0.5 * bookmarks::kBookmarkHorizontalPadding;
+ }
+ } else {
+ NOTREACHED();
+ }
+
+ return x;
+}
+
+- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child {
+ // If the bookmarkbar is not in detached mode, lock bar visibility, forcing
+ // the overlay to stay open when in fullscreen mode.
+ if (![self isInState:bookmarks::kDetachedState] &&
+ ![self isAnimatingToState:bookmarks::kDetachedState]) {
+ BrowserWindowController* browserController =
+ [BrowserWindowController browserWindowControllerForView:[self view]];
+ [browserController lockBarVisibilityForOwner:child
+ withAnimation:NO
+ delay:NO];
+ }
+}
+
+- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child {
+ // Release bar visibility, allowing the overlay to close if in fullscreen
+ // mode.
+ BrowserWindowController* browserController =
+ [BrowserWindowController browserWindowControllerForView:[self view]];
+ [browserController releaseBarVisibilityForOwner:child
+ withAnimation:NO
+ delay:NO];
+}
+
+// Add a new folder controller as triggered by the given folder button.
+- (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton {
+
+ // If doing a close/open, make sure the fullscreen chrome doesn't
+ // have a chance to begin animating away in the middle of things.
+ BrowserWindowController* browserController =
+ [BrowserWindowController browserWindowControllerForView:[self view]];
+ // Confirm we're not re-locking with ourself as an owner before locking.
+ DCHECK([browserController isBarVisibilityLockedForOwner:self] == NO);
+ [browserController lockBarVisibilityForOwner:self
+ withAnimation:NO
+ delay:NO];
+
+ if (folderController_)
+ [self closeAllBookmarkFolders];
+
+ // Folder controller, like many window controllers, owns itself.
+ folderController_ =
+ [[BookmarkBarFolderController alloc] initWithParentButton:parentButton
+ parentController:nil
+ barController:self];
+ [folderController_ showWindow:self];
+
+ // Only BookmarkBarController has this; the
+ // BookmarkBarFolderController does not.
+ [self watchForExitEvent:YES];
+
+ // No longer need to hold the lock; the folderController_ now owns it.
+ [browserController releaseBarVisibilityForOwner:self
+ withAnimation:NO
+ delay:NO];
+}
+
+- (void)openAll:(const BookmarkNode*)node
+ disposition:(WindowOpenDisposition)disposition {
+ [self closeFolderAndStopTrackingMenus];
+ bookmark_utils::OpenAll([[self view] window],
+ browser_->profile(),
+ browser_,
+ node,
+ disposition);
+}
+
+- (void)addButtonForNode:(const BookmarkNode*)node
+ atIndex:(NSInteger)buttonIndex {
+ int newOffset = 0;
+ if (buttonIndex == -1)
+ buttonIndex = [buttons_ count]; // New button goes at the end.
+ if (buttonIndex <= (NSInteger)[buttons_ count]) {
+ if (buttonIndex) {
+ BookmarkButton* targetButton = [buttons_ objectAtIndex:buttonIndex - 1];
+ NSRect targetFrame = [targetButton frame];
+ newOffset = targetFrame.origin.x + NSWidth(targetFrame) +
+ bookmarks::kBookmarkHorizontalPadding;
+ }
+ BookmarkButton* newButton = [self buttonForNode:node xOffset:&newOffset];
+ CGFloat xOffset =
+ NSWidth([newButton frame]) + bookmarks::kBookmarkHorizontalPadding;
+ NSUInteger buttonCount = [buttons_ count];
+ for (NSUInteger i = buttonIndex; i < buttonCount; ++i) {
+ BookmarkButton* button = [buttons_ objectAtIndex:i];
+ NSPoint buttonOrigin = [button frame].origin;
+ buttonOrigin.x += xOffset;
+ [button setFrameOrigin:buttonOrigin];
+ }
+ ++displayedButtonCount_;
+ [buttons_ insertObject:newButton atIndex:buttonIndex];
+ [buttonView_ addSubview:newButton];
+
+ // See if any buttons need to be pushed off to or brought in from the side.
+ [self reconfigureBookmarkBar];
+ } else {
+ // A button from somewhere else (not the bar) is being moved to the
+ // off-the-side so insure it gets redrawn if its showing.
+ [self reconfigureBookmarkBar];
+ [folderController_ reconfigureMenu];
+ }
+}
+
+// TODO(mrossetti): Duplicate code with BookmarkBarFolderController.
+// http://crbug.com/35966
+- (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point {
+ DCHECK([urls count] == [titles count]);
+ BOOL nodesWereAdded = NO;
+ // Figure out where these new bookmarks nodes are to be added.
+ BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
+ const BookmarkNode* destParent = NULL;
+ int destIndex = 0;
+ if ([button isFolder]) {
+ destParent = [button bookmarkNode];
+ // Drop it at the end.
+ destIndex = [button bookmarkNode]->GetChildCount();
+ } else {
+ // Else we're dropping somewhere on the bar, so find the right spot.
+ destParent = bookmarkModel_->GetBookmarkBarNode();
+ destIndex = [self indexForDragToPoint:point];
+ }
+
+ // Don't add the bookmarks if the destination index shows an error.
+ if (destIndex >= 0) {
+ // Create and add the new bookmark nodes.
+ size_t urlCount = [urls count];
+ for (size_t i = 0; i < urlCount; ++i) {
+ GURL gurl;
+ const char* string = [[urls objectAtIndex:i] UTF8String];
+ if (string)
+ gurl = GURL(string);
+ // We only expect to receive valid URLs.
+ DCHECK(gurl.is_valid());
+ if (gurl.is_valid()) {
+ bookmarkModel_->AddURL(destParent,
+ destIndex++,
+ base::SysNSStringToUTF16(
+ [titles objectAtIndex:i]),
+ gurl);
+ nodesWereAdded = YES;
+ }
+ }
+ }
+ return nodesWereAdded;
+}
+
+// TODO(mrossetti): jrg wants this broken up into smaller functions.
+- (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex {
+ if (fromIndex != toIndex) {
+ NSInteger buttonCount = (NSInteger)[buttons_ count];
+ if (toIndex == -1)
+ toIndex = buttonCount;
+ // See if we have a simple move within the bar, which will be the case if
+ // both button indexes are in the visible space.
+ if (fromIndex < buttonCount && toIndex < buttonCount) {
+ BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex];
+ NSRect movedFrame = [movedButton frame];
+ NSPoint toOrigin = movedFrame.origin;
+ CGFloat xOffset =
+ NSWidth(movedFrame) + bookmarks::kBookmarkHorizontalPadding;
+ // Hide the button to reduce flickering while drawing the window.
+ [movedButton setHidden:YES];
+ [buttons_ removeObjectAtIndex:fromIndex];
+ if (fromIndex < toIndex) {
+ // Move the button from left to right within the bar.
+ BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex - 1];
+ NSRect toFrame = [targetButton frame];
+ toOrigin.x = toFrame.origin.x - NSWidth(movedFrame) + NSWidth(toFrame);
+ for (NSInteger i = fromIndex; i < toIndex; ++i) {
+ BookmarkButton* button = [buttons_ objectAtIndex:i];
+ NSRect frame = [button frame];
+ frame.origin.x -= xOffset;
+ [button setFrameOrigin:frame.origin];
+ }
+ } else {
+ // Move the button from right to left within the bar.
+ BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex];
+ toOrigin = [targetButton frame].origin;
+ for (NSInteger i = fromIndex - 1; i >= toIndex; --i) {
+ BookmarkButton* button = [buttons_ objectAtIndex:i];
+ NSRect buttonFrame = [button frame];
+ buttonFrame.origin.x += xOffset;
+ [button setFrameOrigin:buttonFrame.origin];
+ }
+ }
+ [buttons_ insertObject:movedButton atIndex:toIndex];
+ [movedButton setFrameOrigin:toOrigin];
+ [movedButton setHidden:NO];
+ } else if (fromIndex < buttonCount) {
+ // A button is being removed from the bar and added to off-the-side.
+ // By now the node has already been inserted into the model so the
+ // button to be added is represented by |toIndex|. Things get
+ // complicated because the off-the-side is showing and must be redrawn
+ // while possibly re-laying out the bookmark bar.
+ [self removeButton:fromIndex animate:NO];
+ [self reconfigureBookmarkBar];
+ [folderController_ reconfigureMenu];
+ } else if (toIndex < buttonCount) {
+ // A button is being added to the bar and removed from off-the-side.
+ // By now the node has already been inserted into the model so the
+ // button to be added is represented by |toIndex|.
+ const BookmarkNode* node = bookmarkModel_->GetBookmarkBarNode();
+ const BookmarkNode* movedNode = node->GetChild(toIndex);
+ DCHECK(movedNode);
+ [self addButtonForNode:movedNode atIndex:toIndex];
+ [self reconfigureBookmarkBar];
+ } else {
+ // A button is being moved within the off-the-side.
+ fromIndex -= buttonCount;
+ toIndex -= buttonCount;
+ [folderController_ moveButtonFromIndex:fromIndex toIndex:toIndex];
+ }
+ }
+}
+
+- (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate {
+ if (buttonIndex < (NSInteger)[buttons_ count]) {
+ // The button being removed is showing in the bar.
+ BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex];
+ if (oldButton == [folderController_ parentButton]) {
+ // If we are deleting a button whose folder is currently open, close it!
+ [self closeAllBookmarkFolders];
+ }
+ NSPoint poofPoint = [oldButton screenLocationForRemoveAnimation];
+ NSRect oldFrame = [oldButton frame];
+ [oldButton setDelegate:nil];
+ [oldButton removeFromSuperview];
+ if (animate && !ignoreAnimations_ && [self isVisible])
+ NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint,
+ NSZeroSize, nil, nil, nil);
+ CGFloat xOffset = NSWidth(oldFrame) + bookmarks::kBookmarkHorizontalPadding;
+ [buttons_ removeObjectAtIndex:buttonIndex];
+ NSUInteger buttonCount = [buttons_ count];
+ for (NSUInteger i = buttonIndex; i < buttonCount; ++i) {
+ BookmarkButton* button = [buttons_ objectAtIndex:i];
+ NSRect buttonFrame = [button frame];
+ buttonFrame.origin.x -= xOffset;
+ [button setFrame:buttonFrame];
+ // If this button is showing its menu then we need to move the menu, too.
+ if (button == [folderController_ parentButton])
+ [folderController_ offsetFolderMenuWindow:NSMakeSize(xOffset, 0.0)];
+ }
+ --displayedButtonCount_;
+ [self reconfigureBookmarkBar];
+ } else if (folderController_ &&
+ [folderController_ parentButton] == offTheSideButton_) {
+ // The button being removed is in the OTS (off-the-side) and the OTS
+ // menu is showing so we need to remove the button.
+ NSInteger index = buttonIndex - displayedButtonCount_;
+ [folderController_ removeButton:index animate:YES];
+ }
+}
+
+- (id<BookmarkButtonControllerProtocol>)controllerForNode:
+ (const BookmarkNode*)node {
+ // See if it's in the bar, then if it is in the hierarchy of visible
+ // folder menus.
+ if (bookmarkModel_->GetBookmarkBarNode() == node)
+ return self;
+ return [folderController_ controllerForNode:node];
+}
+
+#pragma mark BookmarkButtonControllerProtocol
+
+// NOT an override of a standard Cocoa call made to NSViewControllers.
+- (void)hookForEvent:(NSEvent*)theEvent {
+ if ([self isEventAnExitEvent:theEvent])
+ [self closeFolderAndStopTrackingMenus];
+}
+
+#pragma mark TestingAPI Only
+
+- (NSMenu*)buttonContextMenu {
+ return buttonContextMenu_;
+}
+
+// Intentionally ignores ownership issues; used for testing and we try
+// to minimize touching the object passed in (likely a mock).
+- (void)setButtonContextMenu:(id)menu {
+ buttonContextMenu_ = menu;
+}
+
+- (void)setIgnoreAnimations:(BOOL)ignore {
+ ignoreAnimations_ = ignore;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller_unittest.mm
new file mode 100644
index 0000000..80f6bc7
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller_unittest.mm
@@ -0,0 +1,2169 @@
+// 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 "app/theme_provider.h"
+#include "base/basictypes.h"
+#include "base/scoped_nsobject.h"
+#include "base/string16.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/test_event_utils.h"
+#import "chrome/browser/ui/cocoa/view_resizer_pong.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/model_test_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+
+// Just like a BookmarkBarController but openURL: is stubbed out.
+@interface BookmarkBarControllerNoOpen : BookmarkBarController {
+ @public
+ std::vector<GURL> urls_;
+ std::vector<WindowOpenDisposition> dispositions_;
+}
+@end
+
+@implementation BookmarkBarControllerNoOpen
+- (void)openURL:(GURL)url disposition:(WindowOpenDisposition)disposition {
+ urls_.push_back(url);
+ dispositions_.push_back(disposition);
+}
+- (void)clear {
+ urls_.clear();
+ dispositions_.clear();
+}
+@end
+
+
+// NSCell that is pre-provided with a desired size that becomes the
+// return value for -(NSSize)cellSize:.
+@interface CellWithDesiredSize : NSCell {
+ @private
+ NSSize cellSize_;
+}
+@property (nonatomic, readonly) NSSize cellSize;
+@end
+
+@implementation CellWithDesiredSize
+
+@synthesize cellSize = cellSize_;
+
+- (id)initTextCell:(NSString*)string desiredSize:(NSSize)size {
+ if ((self = [super initTextCell:string])) {
+ cellSize_ = size;
+ }
+ return self;
+}
+
+@end
+
+// Remember the number of times we've gotten a frameDidChange notification.
+@interface BookmarkBarControllerTogglePong : BookmarkBarControllerNoOpen {
+ @private
+ int toggles_;
+}
+@property (nonatomic, readonly) int toggles;
+@end
+
+@implementation BookmarkBarControllerTogglePong
+
+@synthesize toggles = toggles_;
+
+- (void)frameDidChange {
+ toggles_++;
+}
+
+@end
+
+// Remembers if a notification callback was called.
+@interface BookmarkBarControllerNotificationPong : BookmarkBarControllerNoOpen {
+ BOOL windowWillCloseReceived_;
+ BOOL windowDidResignKeyReceived_;
+}
+@property (nonatomic, readonly) BOOL windowWillCloseReceived;
+@property (nonatomic, readonly) BOOL windowDidResignKeyReceived;
+@end
+
+@implementation BookmarkBarControllerNotificationPong
+@synthesize windowWillCloseReceived = windowWillCloseReceived_;
+@synthesize windowDidResignKeyReceived = windowDidResignKeyReceived_;
+
+// Override NSNotificationCenter callback.
+- (void)parentWindowWillClose:(NSNotification*)notification {
+ windowWillCloseReceived_ = YES;
+}
+
+// NSNotificationCenter callback.
+- (void)parentWindowDidResignKey:(NSNotification*)notification {
+ windowDidResignKeyReceived_ = YES;
+}
+@end
+
+// Remembers if and what kind of openAll was performed.
+@interface BookmarkBarControllerOpenAllPong : BookmarkBarControllerNoOpen {
+ WindowOpenDisposition dispositionDetected_;
+}
+@property (nonatomic) WindowOpenDisposition dispositionDetected;
+@end
+
+@implementation BookmarkBarControllerOpenAllPong
+@synthesize dispositionDetected = dispositionDetected_;
+
+// Intercede for the openAll:disposition: method.
+- (void)openAll:(const BookmarkNode*)node
+ disposition:(WindowOpenDisposition)disposition {
+ [self setDispositionDetected:disposition];
+}
+
+@end
+
+// Just like a BookmarkBarController but intercedes when providing
+// pasteboard drag data.
+@interface BookmarkBarControllerDragData : BookmarkBarController {
+ const BookmarkNode* dragDataNode_; // Weak
+}
+- (void)setDragDataNode:(const BookmarkNode*)node;
+@end
+
+@implementation BookmarkBarControllerDragData
+
+- (id)initWithBrowser:(Browser*)browser
+ initialWidth:(CGFloat)initialWidth
+ delegate:(id<BookmarkBarControllerDelegate>)delegate
+ resizeDelegate:(id<ViewResizer>)resizeDelegate {
+ if ((self = [super initWithBrowser:browser
+ initialWidth:initialWidth
+ delegate:delegate
+ resizeDelegate:resizeDelegate])) {
+ dragDataNode_ = NULL;
+ }
+ return self;
+}
+
+- (void)setDragDataNode:(const BookmarkNode*)node {
+ dragDataNode_ = node;
+}
+
+- (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData {
+ std::vector<const BookmarkNode*> dragDataNodes;
+ if(dragDataNode_) {
+ dragDataNodes.push_back(dragDataNode_);
+ }
+ return dragDataNodes;
+}
+
+@end
+
+
+class FakeTheme : public ThemeProvider {
+ public:
+ FakeTheme(NSColor* color) : color_(color) { }
+ scoped_nsobject<NSColor> color_;
+
+ virtual void Init(Profile* profile) { }
+ virtual SkBitmap* GetBitmapNamed(int id) const { return nil; }
+ virtual SkColor GetColor(int id) const { return SkColor(); }
+ virtual bool GetDisplayProperty(int id, int* result) const { return false; }
+ virtual bool ShouldUseNativeFrame() const { return false; }
+ virtual bool HasCustomImage(int id) const { return false; }
+ virtual RefCountedMemory* GetRawData(int id) const { return NULL; }
+ virtual NSImage* GetNSImageNamed(int id, bool allow_default) const {
+ return nil;
+ }
+ virtual NSColor* GetNSImageColorNamed(int id, bool allow_default) const {
+ return nil;
+ }
+ virtual NSColor* GetNSColor(int id, bool allow_default) const {
+ return color_.get();
+ }
+ virtual NSColor* GetNSColorTint(int id, bool allow_default) const {
+ return nil;
+ }
+ virtual NSGradient* GetNSGradient(int id) const {
+ return nil;
+ }
+};
+
+
+@interface FakeDragInfo : NSObject {
+ @public
+ NSPoint dropLocation_;
+ NSDragOperation sourceMask_;
+}
+@property (nonatomic, assign) NSPoint dropLocation;
+- (void)setDraggingSourceOperationMask:(NSDragOperation)mask;
+@end
+
+@implementation FakeDragInfo
+
+@synthesize dropLocation = dropLocation_;
+
+- (id)init {
+ if ((self = [super init])) {
+ dropLocation_ = NSZeroPoint;
+ sourceMask_ = NSDragOperationMove;
+ }
+ return self;
+}
+
+// NSDraggingInfo protocol functions.
+
+- (id)draggingPasteboard {
+ return self;
+}
+
+- (id)draggingSource {
+ return self;
+}
+
+- (NSDragOperation)draggingSourceOperationMask {
+ return sourceMask_;
+}
+
+- (NSPoint)draggingLocation {
+ return dropLocation_;
+}
+
+// Other functions.
+
+- (void)setDraggingSourceOperationMask:(NSDragOperation)mask {
+ sourceMask_ = mask;
+}
+
+@end
+
+
+namespace {
+
+class BookmarkBarControllerTestBase : public CocoaTest {
+ public:
+ BrowserTestHelper helper_;
+ scoped_nsobject<NSView> parent_view_;
+ scoped_nsobject<ViewResizerPong> resizeDelegate_;
+
+ BookmarkBarControllerTestBase() {
+ resizeDelegate_.reset([[ViewResizerPong alloc] init]);
+ NSRect parent_frame = NSMakeRect(0, 0, 800, 50);
+ parent_view_.reset([[NSView alloc] initWithFrame:parent_frame]);
+ [parent_view_ setHidden:YES];
+ }
+
+ void InstallAndToggleBar(BookmarkBarController* bar) {
+ // Force loading of the nib.
+ [bar view];
+ // Awkwardness to look like we've been installed.
+ for (NSView* subView in [parent_view_ subviews])
+ [subView removeFromSuperview];
+ [parent_view_ addSubview:[bar view]];
+ NSRect frame = [[[bar view] superview] frame];
+ frame.origin.y = 100;
+ [[[bar view] superview] setFrame:frame];
+
+ // Make sure it's on in a window so viewDidMoveToWindow is called
+ NSView* contentView = [test_window() contentView];
+ if (![parent_view_ isDescendantOf:contentView])
+ [contentView addSubview:parent_view_];
+
+ // Make sure it's open so certain things aren't no-ops.
+ [bar updateAndShowNormalBar:YES
+ showDetachedBar:NO
+ withAnimation:NO];
+ }
+};
+
+class BookmarkBarControllerTest : public BookmarkBarControllerTestBase {
+ public:
+ scoped_nsobject<BookmarkMenu> menu_;
+ scoped_nsobject<NSMenuItem> menu_item_;
+ scoped_nsobject<NSButtonCell> cell_;
+ scoped_nsobject<BookmarkBarControllerNoOpen> bar_;
+
+ BookmarkBarControllerTest() {
+ bar_.reset(
+ [[BookmarkBarControllerNoOpen alloc]
+ initWithBrowser:helper_.browser()
+ initialWidth:NSWidth([parent_view_ frame])
+ delegate:nil
+ resizeDelegate:resizeDelegate_.get()]);
+
+ InstallAndToggleBar(bar_.get());
+
+ // Create a menu/item to act like a sender
+ menu_.reset([[BookmarkMenu alloc] initWithTitle:@"I_dont_care"]);
+ menu_item_.reset([[NSMenuItem alloc]
+ initWithTitle:@"still_dont_care"
+ action:NULL
+ keyEquivalent:@""]);
+ cell_.reset([[NSButtonCell alloc] init]);
+ [menu_item_ setMenu:menu_.get()];
+ [menu_ setDelegate:cell_.get()];
+ }
+
+ // Return a menu item that points to the given URL.
+ NSMenuItem* ItemForBookmarkBarMenu(GURL& gurl) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ const BookmarkNode* node = model->AddURL(parent, parent->GetChildCount(),
+ ASCIIToUTF16("A title"), gurl);
+ [menu_ setRepresentedObject:[NSNumber numberWithLongLong:node->id()]];
+ return menu_item_;
+ }
+
+ // Does NOT take ownership of node.
+ NSMenuItem* ItemForBookmarkBarMenu(const BookmarkNode* node) {
+ [menu_ setRepresentedObject:[NSNumber numberWithLongLong:node->id()]];
+ return menu_item_;
+ }
+
+ BookmarkBarControllerNoOpen* noOpenBar() {
+ return (BookmarkBarControllerNoOpen*)bar_.get();
+ }
+};
+
+TEST_F(BookmarkBarControllerTest, ShowWhenShowBookmarkBarTrue) {
+ [bar_ updateAndShowNormalBar:YES
+ showDetachedBar:NO
+ withAnimation:NO];
+ EXPECT_TRUE([bar_ isInState:bookmarks::kShowingState]);
+ EXPECT_FALSE([bar_ isInState:bookmarks::kDetachedState]);
+ EXPECT_TRUE([bar_ isVisible]);
+ EXPECT_FALSE([bar_ isAnimationRunning]);
+ EXPECT_FALSE([[bar_ view] isHidden]);
+ EXPECT_GT([resizeDelegate_ height], 0);
+ EXPECT_GT([[bar_ view] frame].size.height, 0);
+}
+
+TEST_F(BookmarkBarControllerTest, HideWhenShowBookmarkBarFalse) {
+ [bar_ updateAndShowNormalBar:NO
+ showDetachedBar:NO
+ withAnimation:NO];
+ EXPECT_FALSE([bar_ isInState:bookmarks::kShowingState]);
+ EXPECT_FALSE([bar_ isInState:bookmarks::kDetachedState]);
+ EXPECT_FALSE([bar_ isVisible]);
+ EXPECT_FALSE([bar_ isAnimationRunning]);
+ EXPECT_TRUE([[bar_ view] isHidden]);
+ EXPECT_EQ(0, [resizeDelegate_ height]);
+ EXPECT_EQ(0, [[bar_ view] frame].size.height);
+}
+
+TEST_F(BookmarkBarControllerTest, HideWhenShowBookmarkBarTrueButDisabled) {
+ [bar_ setBookmarkBarEnabled:NO];
+ [bar_ updateAndShowNormalBar:YES
+ showDetachedBar:NO
+ withAnimation:NO];
+ EXPECT_TRUE([bar_ isInState:bookmarks::kShowingState]);
+ EXPECT_FALSE([bar_ isInState:bookmarks::kDetachedState]);
+ EXPECT_FALSE([bar_ isVisible]);
+ EXPECT_FALSE([bar_ isAnimationRunning]);
+ EXPECT_TRUE([[bar_ view] isHidden]);
+ EXPECT_EQ(0, [resizeDelegate_ height]);
+ EXPECT_EQ(0, [[bar_ view] frame].size.height);
+}
+
+TEST_F(BookmarkBarControllerTest, ShowOnNewTabPage) {
+ [bar_ updateAndShowNormalBar:NO
+ showDetachedBar:YES
+ withAnimation:NO];
+ EXPECT_FALSE([bar_ isInState:bookmarks::kShowingState]);
+ EXPECT_TRUE([bar_ isInState:bookmarks::kDetachedState]);
+ EXPECT_TRUE([bar_ isVisible]);
+ EXPECT_FALSE([bar_ isAnimationRunning]);
+ EXPECT_FALSE([[bar_ view] isHidden]);
+ EXPECT_GT([resizeDelegate_ height], 0);
+ EXPECT_GT([[bar_ view] frame].size.height, 0);
+
+ // Make sure no buttons fall off the bar, either now or when resized
+ // bigger or smaller.
+ CGFloat sizes[] = { 300.0, -100.0, 200.0, -420.0 };
+ CGFloat previousX = 0.0;
+ for (unsigned x = 0; x < arraysize(sizes); x++) {
+ // Confirm the buttons moved from the last check (which may be
+ // init but that's fine).
+ CGFloat newX = [[bar_ offTheSideButton] frame].origin.x;
+ EXPECT_NE(previousX, newX);
+ previousX = newX;
+
+ // Confirm the buttons have a reasonable bounds. Recall that |-frame|
+ // returns rectangles in the superview's coordinates.
+ NSRect buttonViewFrame =
+ [[bar_ buttonView] convertRect:[[bar_ buttonView] frame]
+ fromView:[[bar_ buttonView] superview]];
+ EXPECT_EQ([bar_ buttonView], [[bar_ offTheSideButton] superview]);
+ EXPECT_TRUE(NSContainsRect(buttonViewFrame,
+ [[bar_ offTheSideButton] frame]));
+ EXPECT_EQ([bar_ buttonView], [[bar_ otherBookmarksButton] superview]);
+ EXPECT_TRUE(NSContainsRect(buttonViewFrame,
+ [[bar_ otherBookmarksButton] frame]));
+
+ // Now move them implicitly.
+ // We confirm FrameChangeNotification works in the next unit test;
+ // we simply assume it works here to resize or reposition the
+ // buttons above.
+ NSRect frame = [[bar_ view] frame];
+ frame.size.width += sizes[x];
+ [[bar_ view] setFrame:frame];
+ }
+}
+
+// Test whether |-updateAndShowNormalBar:...| sets states as we expect. Make
+// sure things don't crash.
+TEST_F(BookmarkBarControllerTest, StateChanges) {
+ // First, go in one-at-a-time cycle.
+ [bar_ updateAndShowNormalBar:NO
+ showDetachedBar:NO
+ withAnimation:NO];
+ EXPECT_EQ(bookmarks::kHiddenState, [bar_ visualState]);
+ EXPECT_FALSE([bar_ isVisible]);
+ EXPECT_FALSE([bar_ isAnimationRunning]);
+ [bar_ updateAndShowNormalBar:YES
+ showDetachedBar:NO
+ withAnimation:NO];
+ EXPECT_EQ(bookmarks::kShowingState, [bar_ visualState]);
+ EXPECT_TRUE([bar_ isVisible]);
+ EXPECT_FALSE([bar_ isAnimationRunning]);
+ [bar_ updateAndShowNormalBar:YES
+ showDetachedBar:YES
+ withAnimation:NO];
+ EXPECT_EQ(bookmarks::kShowingState, [bar_ visualState]);
+ EXPECT_TRUE([bar_ isVisible]);
+ EXPECT_FALSE([bar_ isAnimationRunning]);
+ [bar_ updateAndShowNormalBar:NO
+ showDetachedBar:YES
+ withAnimation:NO];
+ EXPECT_EQ(bookmarks::kDetachedState, [bar_ visualState]);
+ EXPECT_TRUE([bar_ isVisible]);
+ EXPECT_FALSE([bar_ isAnimationRunning]);
+
+ // Now try some "jumps".
+ for (int i = 0; i < 2; i++) {
+ [bar_ updateAndShowNormalBar:NO
+ showDetachedBar:NO
+ withAnimation:NO];
+ EXPECT_EQ(bookmarks::kHiddenState, [bar_ visualState]);
+ EXPECT_FALSE([bar_ isVisible]);
+ EXPECT_FALSE([bar_ isAnimationRunning]);
+ [bar_ updateAndShowNormalBar:YES
+ showDetachedBar:YES
+ withAnimation:NO];
+ EXPECT_EQ(bookmarks::kShowingState, [bar_ visualState]);
+ EXPECT_TRUE([bar_ isVisible]);
+ EXPECT_FALSE([bar_ isAnimationRunning]);
+ }
+
+ // Now try some "jumps".
+ for (int i = 0; i < 2; i++) {
+ [bar_ updateAndShowNormalBar:YES
+ showDetachedBar:NO
+ withAnimation:NO];
+ EXPECT_EQ(bookmarks::kShowingState, [bar_ visualState]);
+ EXPECT_TRUE([bar_ isVisible]);
+ EXPECT_FALSE([bar_ isAnimationRunning]);
+ [bar_ updateAndShowNormalBar:NO
+ showDetachedBar:YES
+ withAnimation:NO];
+ EXPECT_EQ(bookmarks::kDetachedState, [bar_ visualState]);
+ EXPECT_TRUE([bar_ isVisible]);
+ EXPECT_FALSE([bar_ isAnimationRunning]);
+ }
+}
+
+// Make sure we're watching for frame change notifications.
+TEST_F(BookmarkBarControllerTest, FrameChangeNotification) {
+ scoped_nsobject<BookmarkBarControllerTogglePong> bar;
+ bar.reset(
+ [[BookmarkBarControllerTogglePong alloc]
+ initWithBrowser:helper_.browser()
+ initialWidth:100 // arbitrary
+ delegate:nil
+ resizeDelegate:resizeDelegate_.get()]);
+ InstallAndToggleBar(bar.get());
+
+ // Send a frame did change notification for the pong's view.
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:NSViewFrameDidChangeNotification
+ object:[bar view]];
+
+ EXPECT_GT([bar toggles], 0);
+}
+
+// Confirm our "no items" container goes away when we add the 1st
+// bookmark, and comes back when we delete the bookmark.
+TEST_F(BookmarkBarControllerTest, NoItemContainerGoesAway) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* bar = model->GetBookmarkBarNode();
+
+ [bar_ loaded:model];
+ BookmarkBarView* view = [bar_ buttonView];
+ DCHECK(view);
+ NSView* noItemContainer = [view noItemContainer];
+ DCHECK(noItemContainer);
+
+ EXPECT_FALSE([noItemContainer isHidden]);
+ const BookmarkNode* node = model->AddURL(bar, bar->GetChildCount(),
+ ASCIIToUTF16("title"),
+ GURL("http://www.google.com"));
+ EXPECT_TRUE([noItemContainer isHidden]);
+ model->Remove(bar, bar->IndexOfChild(node));
+ EXPECT_FALSE([noItemContainer isHidden]);
+
+ // Now try it using a bookmark from the Other Bookmarks.
+ const BookmarkNode* otherBookmarks = model->other_node();
+ node = model->AddURL(otherBookmarks, otherBookmarks->GetChildCount(),
+ ASCIIToUTF16("TheOther"),
+ GURL("http://www.other.com"));
+ EXPECT_FALSE([noItemContainer isHidden]);
+ // Move it from Other Bookmarks to the bar.
+ model->Move(node, bar, 0);
+ EXPECT_TRUE([noItemContainer isHidden]);
+ // Move it back to Other Bookmarks from the bar.
+ model->Move(node, otherBookmarks, 0);
+ EXPECT_FALSE([noItemContainer isHidden]);
+}
+
+// Confirm off the side button only enabled when reasonable.
+TEST_F(BookmarkBarControllerTest, OffTheSideButtonHidden) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ [bar_ setIgnoreAnimations:YES];
+
+ [bar_ loaded:model];
+ EXPECT_TRUE([bar_ offTheSideButtonIsHidden]);
+
+ for (int i = 0; i < 2; i++) {
+ model->SetURLStarred(GURL("http://www.foo.com"), ASCIIToUTF16("small"),
+ true);
+ EXPECT_TRUE([bar_ offTheSideButtonIsHidden]);
+ }
+
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ for (int i = 0; i < 20; i++) {
+ model->AddURL(parent, parent->GetChildCount(),
+ ASCIIToUTF16("super duper wide title"),
+ GURL("http://superfriends.hall-of-justice.edu"));
+ }
+ EXPECT_FALSE([bar_ offTheSideButtonIsHidden]);
+
+ // Open the "off the side" and start deleting nodes. Make sure
+ // deletion of the last node in "off the side" causes the folder to
+ // close.
+ EXPECT_FALSE([bar_ offTheSideButtonIsHidden]);
+ NSButton* offTheSideButton = [bar_ offTheSideButton];
+ // Open "off the side" menu.
+ [bar_ openOffTheSideFolderFromButton:offTheSideButton];
+ BookmarkBarFolderController* bbfc = [bar_ folderController];
+ EXPECT_TRUE(bbfc);
+ [bbfc setIgnoreAnimations:YES];
+ while (parent->GetChildCount()) {
+ // We've completed the job so we're done.
+ if ([bar_ offTheSideButtonIsHidden])
+ break;
+ // Delete the last button.
+ model->Remove(parent, parent->GetChildCount()-1);
+ // If last one make sure the menu is closed and the button is hidden.
+ // Else make sure menu stays open.
+ if ([bar_ offTheSideButtonIsHidden]) {
+ EXPECT_FALSE([bar_ folderController]);
+ } else {
+ EXPECT_TRUE([bar_ folderController]);
+ }
+ }
+}
+
+// http://crbug.com/46175 is a crash when deleting bookmarks from the
+// off-the-side menu while it is open. This test tries to bang hard
+// in this area to reproduce the crash.
+TEST_F(BookmarkBarControllerTest, DeleteFromOffTheSideWhileItIsOpen) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ [bar_ setIgnoreAnimations:YES];
+ [bar_ loaded:model];
+
+ // Add a lot of bookmarks (per the bug).
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ for (int i = 0; i < 100; i++) {
+ std::ostringstream title;
+ title << "super duper wide title " << i;
+ model->AddURL(parent, parent->GetChildCount(), ASCIIToUTF16(title.str()),
+ GURL("http://superfriends.hall-of-justice.edu"));
+ }
+ EXPECT_FALSE([bar_ offTheSideButtonIsHidden]);
+
+ // Open "off the side" menu.
+ NSButton* offTheSideButton = [bar_ offTheSideButton];
+ [bar_ openOffTheSideFolderFromButton:offTheSideButton];
+ BookmarkBarFolderController* bbfc = [bar_ folderController];
+ EXPECT_TRUE(bbfc);
+ [bbfc setIgnoreAnimations:YES];
+
+ // Start deleting items; try and delete randomish ones in case it
+ // makes a difference.
+ int indices[] = { 2, 4, 5, 1, 7, 9, 2, 0, 10, 9 };
+ while (parent->GetChildCount()) {
+ for (unsigned int i = 0; i < arraysize(indices); i++) {
+ if (indices[i] < parent->GetChildCount()) {
+ // First we mouse-enter the button to make things harder.
+ NSArray* buttons = [bbfc buttons];
+ for (BookmarkButton* button in buttons) {
+ if ([button bookmarkNode] == parent->GetChild(indices[i])) {
+ [bbfc mouseEnteredButton:button event:nil];
+ break;
+ }
+ }
+ // Then we remove the node. This triggers the button to get
+ // deleted.
+ model->Remove(parent, indices[i]);
+ // Force visual update which is otherwise delayed.
+ [[bbfc window] displayIfNeeded];
+ }
+ }
+ }
+}
+
+// Test whether |-dragShouldLockBarVisibility| returns NO iff the bar is
+// detached.
+TEST_F(BookmarkBarControllerTest, TestDragShouldLockBarVisibility) {
+ [bar_ updateAndShowNormalBar:NO
+ showDetachedBar:NO
+ withAnimation:NO];
+ EXPECT_TRUE([bar_ dragShouldLockBarVisibility]);
+
+ [bar_ updateAndShowNormalBar:YES
+ showDetachedBar:NO
+ withAnimation:NO];
+ EXPECT_TRUE([bar_ dragShouldLockBarVisibility]);
+
+ [bar_ updateAndShowNormalBar:YES
+ showDetachedBar:YES
+ withAnimation:NO];
+ EXPECT_TRUE([bar_ dragShouldLockBarVisibility]);
+
+ [bar_ updateAndShowNormalBar:NO
+ showDetachedBar:YES
+ withAnimation:NO];
+ EXPECT_FALSE([bar_ dragShouldLockBarVisibility]);
+}
+
+TEST_F(BookmarkBarControllerTest, TagMap) {
+ int64 ids[] = { 1, 3, 4, 40, 400, 4000, 800000000, 2, 123456789 };
+ std::vector<int32> tags;
+
+ // Generate some tags
+ for (unsigned int i = 0; i < arraysize(ids); i++) {
+ tags.push_back([bar_ menuTagFromNodeId:ids[i]]);
+ }
+
+ // Confirm reverse mapping.
+ for (unsigned int i = 0; i < arraysize(ids); i++) {
+ EXPECT_EQ(ids[i], [bar_ nodeIdFromMenuTag:tags[i]]);
+ }
+
+ // Confirm uniqueness.
+ std::sort(tags.begin(), tags.end());
+ for (unsigned int i=0; i<(tags.size()-1); i++) {
+ EXPECT_NE(tags[i], tags[i+1]);
+ }
+}
+
+TEST_F(BookmarkBarControllerTest, MenuForFolderNode) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+
+ // First make sure something (e.g. "(empty)" string) is always present.
+ NSMenu* menu = [bar_ menuForFolderNode:model->GetBookmarkBarNode()];
+ EXPECT_GT([menu numberOfItems], 0);
+
+ // Test two bookmarks.
+ GURL gurl("http://www.foo.com");
+ model->SetURLStarred(gurl, ASCIIToUTF16("small"), true);
+ model->SetURLStarred(GURL("http://www.cnn.com"), ASCIIToUTF16("bigger title"),
+ true);
+ menu = [bar_ menuForFolderNode:model->GetBookmarkBarNode()];
+ EXPECT_EQ([menu numberOfItems], 2);
+ NSMenuItem *item = [menu itemWithTitle:@"bigger title"];
+ EXPECT_TRUE(item);
+ item = [menu itemWithTitle:@"small"];
+ EXPECT_TRUE(item);
+ if (item) {
+ int64 tag = [bar_ nodeIdFromMenuTag:[item tag]];
+ const BookmarkNode* node = model->GetNodeByID(tag);
+ EXPECT_TRUE(node);
+ EXPECT_EQ(gurl, node->GetURL());
+ }
+
+ // Test with an actual folder as well
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ const BookmarkNode* folder = model->AddGroup(parent,
+ parent->GetChildCount(),
+ ASCIIToUTF16("group"));
+ model->AddURL(folder, folder->GetChildCount(),
+ ASCIIToUTF16("f1"), GURL("http://framma-lamma.com"));
+ model->AddURL(folder, folder->GetChildCount(),
+ ASCIIToUTF16("f2"), GURL("http://framma-lamma-ding-dong.com"));
+ menu = [bar_ menuForFolderNode:model->GetBookmarkBarNode()];
+ EXPECT_EQ([menu numberOfItems], 3);
+
+ item = [menu itemWithTitle:@"group"];
+ EXPECT_TRUE(item);
+ EXPECT_TRUE([item hasSubmenu]);
+ NSMenu *submenu = [item submenu];
+ EXPECT_TRUE(submenu);
+ EXPECT_EQ(2, [submenu numberOfItems]);
+ EXPECT_TRUE([submenu itemWithTitle:@"f1"]);
+ EXPECT_TRUE([submenu itemWithTitle:@"f2"]);
+}
+
+// Confirm openBookmark: forwards the request to the controller's delegate
+TEST_F(BookmarkBarControllerTest, OpenBookmark) {
+ GURL gurl("http://walla.walla.ding.dong.com");
+ scoped_ptr<BookmarkNode> node(new BookmarkNode(gurl));
+
+ 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()]];
+
+ [bar_ openBookmark:button];
+ EXPECT_EQ(noOpenBar()->urls_[0], node->GetURL());
+ EXPECT_EQ(noOpenBar()->dispositions_[0], CURRENT_TAB);
+}
+
+// Confirm opening of bookmarks works from the menus (different
+// dispositions than clicking on the button).
+TEST_F(BookmarkBarControllerTest, OpenBookmarkFromMenus) {
+ const char* urls[] = { "http://walla.walla.ding.dong.com",
+ "http://i_dont_know.com",
+ "http://cee.enn.enn.dot.com" };
+ SEL selectors[] = { @selector(openBookmarkInNewForegroundTab:),
+ @selector(openBookmarkInNewWindow:),
+ @selector(openBookmarkInIncognitoWindow:) };
+ WindowOpenDisposition dispositions[] = { NEW_FOREGROUND_TAB,
+ NEW_WINDOW,
+ OFF_THE_RECORD };
+ for (unsigned int i = 0; i < arraysize(dispositions); i++) {
+ GURL gurl(urls[i]);
+ [bar_ performSelector:selectors[i]
+ withObject:ItemForBookmarkBarMenu(gurl)];
+ EXPECT_EQ(noOpenBar()->urls_[0], gurl);
+ EXPECT_EQ(noOpenBar()->dispositions_[0], dispositions[i]);
+ [bar_ clear];
+ }
+}
+
+TEST_F(BookmarkBarControllerTest, TestAddRemoveAndClear) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ NSView* buttonView = [bar_ buttonView];
+ EXPECT_EQ(0U, [[bar_ buttons] count]);
+ unsigned int initial_subview_count = [[buttonView subviews] count];
+
+ // Make sure a redundant call doesn't choke
+ [bar_ clearBookmarkBar];
+ EXPECT_EQ(0U, [[bar_ buttons] count]);
+ EXPECT_EQ(initial_subview_count, [[buttonView subviews] count]);
+
+ GURL gurl1("http://superfriends.hall-of-justice.edu");
+ // Short titles increase the chances of this test succeeding if the view is
+ // narrow.
+ // TODO(viettrungluu): make the test independent of window/view size, font
+ // metrics, button size and spacing, and everything else.
+ string16 title1(ASCIIToUTF16("x"));
+ model->SetURLStarred(gurl1, title1, true);
+ EXPECT_EQ(1U, [[bar_ buttons] count]);
+ EXPECT_EQ(1+initial_subview_count, [[buttonView subviews] count]);
+
+ GURL gurl2("http://legion-of-doom.gov");
+ string16 title2(ASCIIToUTF16("y"));
+ model->SetURLStarred(gurl2, title2, true);
+ EXPECT_EQ(2U, [[bar_ buttons] count]);
+ EXPECT_EQ(2+initial_subview_count, [[buttonView subviews] count]);
+
+ for (int i = 0; i < 3; i++) {
+ // is_starred=false --> remove the bookmark
+ model->SetURLStarred(gurl2, title2, false);
+ EXPECT_EQ(1U, [[bar_ buttons] count]);
+ EXPECT_EQ(1+initial_subview_count, [[buttonView subviews] count]);
+
+ // and bring it back
+ model->SetURLStarred(gurl2, title2, true);
+ EXPECT_EQ(2U, [[bar_ buttons] count]);
+ EXPECT_EQ(2+initial_subview_count, [[buttonView subviews] count]);
+ }
+
+ [bar_ clearBookmarkBar];
+ EXPECT_EQ(0U, [[bar_ buttons] count]);
+ EXPECT_EQ(initial_subview_count, [[buttonView subviews] count]);
+
+ // Explicit test of loaded: since this is a convenient spot
+ [bar_ loaded:model];
+ EXPECT_EQ(2U, [[bar_ buttons] count]);
+ EXPECT_EQ(2+initial_subview_count, [[buttonView subviews] count]);
+}
+
+// Make sure we don't create too many buttons; we only really need
+// ones that will be visible.
+TEST_F(BookmarkBarControllerTest, TestButtonLimits) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ EXPECT_EQ(0U, [[bar_ buttons] count]);
+ // Add one; make sure we see it.
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ model->AddURL(parent, parent->GetChildCount(),
+ ASCIIToUTF16("title"), GURL("http://www.google.com"));
+ EXPECT_EQ(1U, [[bar_ buttons] count]);
+
+ // Add 30 which we expect to be 'too many'. Make sure we don't see
+ // 30 buttons.
+ model->Remove(parent, 0);
+ EXPECT_EQ(0U, [[bar_ buttons] count]);
+ for (int i=0; i<30; i++) {
+ model->AddURL(parent, parent->GetChildCount(),
+ ASCIIToUTF16("title"), GURL("http://www.google.com"));
+ }
+ int count = [[bar_ buttons] count];
+ EXPECT_LT(count, 30L);
+
+ // Add 10 more (to the front of the list so the on-screen buttons
+ // would change) and make sure the count stays the same.
+ for (int i=0; i<10; i++) {
+ model->AddURL(parent, 0, /* index is 0, so front, not end */
+ ASCIIToUTF16("title"), GURL("http://www.google.com"));
+ }
+
+ // Finally, grow the view and make sure the button count goes up.
+ NSRect frame = [[bar_ view] frame];
+ frame.size.width += 600;
+ [[bar_ view] setFrame:frame];
+ int finalcount = [[bar_ buttons] count];
+ EXPECT_GT(finalcount, count);
+}
+
+// Make sure that each button we add marches to the right and does not
+// overlap with the previous one.
+TEST_F(BookmarkBarControllerTest, TestButtonMarch) {
+ scoped_nsobject<NSMutableArray> cells([[NSMutableArray alloc] init]);
+
+ CGFloat widths[] = { 10, 10, 100, 10, 500, 500, 80000, 60000, 1, 345 };
+ for (unsigned int i = 0; i < arraysize(widths); i++) {
+ NSCell* cell = [[CellWithDesiredSize alloc]
+ initTextCell:@"foo"
+ desiredSize:NSMakeSize(widths[i], 30)];
+ [cells addObject:cell];
+ [cell release];
+ }
+
+ int x_offset = 0;
+ CGFloat x_end = x_offset; // end of the previous button
+ for (unsigned int i = 0; i < arraysize(widths); i++) {
+ NSRect r = [bar_ frameForBookmarkButtonFromCell:[cells objectAtIndex:i]
+ xOffset:&x_offset];
+ EXPECT_GE(r.origin.x, x_end);
+ x_end = NSMaxX(r);
+ }
+}
+
+TEST_F(BookmarkBarControllerTest, CheckForGrowth) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ GURL gurl1("http://www.google.com");
+ string16 title1(ASCIIToUTF16("x"));
+ model->SetURLStarred(gurl1, title1, true);
+
+ GURL gurl2("http://www.google.com/blah");
+ string16 title2(ASCIIToUTF16("y"));
+ model->SetURLStarred(gurl2, title2, true);
+
+ EXPECT_EQ(2U, [[bar_ buttons] count]);
+ CGFloat width_1 = [[[bar_ buttons] objectAtIndex:0] frame].size.width;
+ CGFloat x_2 = [[[bar_ buttons] objectAtIndex:1] frame].origin.x;
+
+ NSButton* first = [[bar_ buttons] objectAtIndex:0];
+ [[first cell] setTitle:@"This is a really big title; watch out mom!"];
+ [bar_ checkForBookmarkButtonGrowth:first];
+
+ // Make sure the 1st button is now wider, the 2nd one is moved over,
+ // and they don't overlap.
+ NSRect frame_1 = [[[bar_ buttons] objectAtIndex:0] frame];
+ NSRect frame_2 = [[[bar_ buttons] objectAtIndex:1] frame];
+ EXPECT_GT(frame_1.size.width, width_1);
+ EXPECT_GT(frame_2.origin.x, x_2);
+ EXPECT_GE(frame_2.origin.x, frame_1.origin.x + frame_1.size.width);
+}
+
+TEST_F(BookmarkBarControllerTest, DeleteBookmark) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+
+ const char* urls[] = { "https://secret.url.com",
+ "http://super.duper.web.site.for.doodz.gov",
+ "http://www.foo-bar-baz.com/" };
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ for (unsigned int i = 0; i < arraysize(urls); i++) {
+ model->AddURL(parent, parent->GetChildCount(),
+ ASCIIToUTF16("title"), GURL(urls[i]));
+ }
+ EXPECT_EQ(3, parent->GetChildCount());
+ const BookmarkNode* middle_node = parent->GetChild(1);
+
+ NSMenuItem* item = ItemForBookmarkBarMenu(middle_node);
+ [bar_ deleteBookmark:item];
+ EXPECT_EQ(2, parent->GetChildCount());
+ EXPECT_EQ(parent->GetChild(0)->GetURL(), GURL(urls[0]));
+ // node 2 moved into spot 1
+ EXPECT_EQ(parent->GetChild(1)->GetURL(), GURL(urls[2]));
+}
+
+// TODO(jrg): write a test to confirm that nodeFavIconLoaded calls
+// checkForBookmarkButtonGrowth:.
+
+TEST_F(BookmarkBarControllerTest, Cell) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ [bar_ loaded:model];
+
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ model->AddURL(parent, parent->GetChildCount(),
+ ASCIIToUTF16("supertitle"),
+ GURL("http://superfriends.hall-of-justice.edu"));
+ const BookmarkNode* node = parent->GetChild(0);
+
+ NSCell* cell = [bar_ cellForBookmarkNode:node];
+ EXPECT_TRUE(cell);
+ EXPECT_NSEQ(@"supertitle", [cell title]);
+ 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
+}
+
+// Test drawing, mostly to ensure nothing leaks or crashes.
+TEST_F(BookmarkBarControllerTest, Display) {
+ [[bar_ view] display];
+}
+
+// Test that middle clicking on a bookmark button results in an open action.
+TEST_F(BookmarkBarControllerTest, MiddleClick) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ GURL gurl1("http://www.google.com/");
+ string16 title1(ASCIIToUTF16("x"));
+ model->SetURLStarred(gurl1, title1, true);
+
+ EXPECT_EQ(1U, [[bar_ buttons] count]);
+ NSButton* first = [[bar_ buttons] objectAtIndex:0];
+ EXPECT_TRUE(first);
+
+ [first otherMouseUp:test_event_utils::MakeMouseEvent(NSOtherMouseUp, 0)];
+ EXPECT_EQ(noOpenBar()->urls_.size(), 1U);
+}
+
+TEST_F(BookmarkBarControllerTest, DisplaysHelpMessageOnEmpty) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ [bar_ loaded:model];
+ EXPECT_FALSE([[[bar_ buttonView] noItemContainer] isHidden]);
+}
+
+TEST_F(BookmarkBarControllerTest, HidesHelpMessageWithBookmark) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ model->AddURL(parent, parent->GetChildCount(),
+ ASCIIToUTF16("title"), GURL("http://one.com"));
+
+ [bar_ loaded:model];
+ EXPECT_TRUE([[[bar_ buttonView] noItemContainer] isHidden]);
+}
+
+TEST_F(BookmarkBarControllerTest, BookmarkButtonSizing) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ model->AddURL(parent, parent->GetChildCount(),
+ ASCIIToUTF16("title"), GURL("http://one.com"));
+
+ [bar_ loaded:model];
+
+ // Make sure the internal bookmark button also is the correct height.
+ NSArray* buttons = [bar_ buttons];
+ EXPECT_GT([buttons count], 0u);
+ for (NSButton* button in buttons) {
+ EXPECT_FLOAT_EQ(
+ (bookmarks::kBookmarkBarHeight + bookmarks::kVisualHeightOffset) - 2 *
+ bookmarks::kBookmarkVerticalPadding,
+ [button frame].size.height);
+ }
+}
+
+TEST_F(BookmarkBarControllerTest, DropBookmarks) {
+ const char* urls[] = {
+ "http://qwantz.com",
+ "http://xkcd.com",
+ "javascript:alert('lolwut')",
+ "file://localhost/tmp/local-file.txt" // As if dragged from the desktop.
+ };
+ const char* titles[] = {
+ "Philosophoraptor",
+ "Can't draw",
+ "Inspiration",
+ "Frum stuf"
+ };
+ EXPECT_EQ(arraysize(urls), arraysize(titles));
+
+ NSMutableArray* nsurls = [NSMutableArray array];
+ NSMutableArray* nstitles = [NSMutableArray array];
+ for (size_t i = 0; i < arraysize(urls); ++i) {
+ [nsurls addObject:base::SysUTF8ToNSString(urls[i])];
+ [nstitles addObject:base::SysUTF8ToNSString(titles[i])];
+ }
+
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ [bar_ addURLs:nsurls withTitles:nstitles at:NSZeroPoint];
+ EXPECT_EQ(4, parent->GetChildCount());
+ for (int i = 0; i < parent->GetChildCount(); ++i) {
+ GURL gurl = parent->GetChild(i)->GetURL();
+ if (gurl.scheme() == "http" ||
+ gurl.scheme() == "javascript") {
+ EXPECT_EQ(parent->GetChild(i)->GetURL(), GURL(urls[i]));
+ } else {
+ // Be flexible if the scheme needed to be added.
+ std::string gurl_string = gurl.spec();
+ std::string my_string = parent->GetChild(i)->GetURL().spec();
+ EXPECT_NE(gurl_string.find(my_string), std::string::npos);
+ }
+ EXPECT_EQ(parent->GetChild(i)->GetTitle(), ASCIIToUTF16(titles[i]));
+ }
+}
+
+TEST_F(BookmarkBarControllerTest, TestButtonOrBar) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ GURL gurl1("http://www.google.com");
+ string16 title1(ASCIIToUTF16("x"));
+ model->SetURLStarred(gurl1, title1, true);
+
+ GURL gurl2("http://www.google.com/gurl_power");
+ string16 title2(ASCIIToUTF16("gurl power"));
+ model->SetURLStarred(gurl2, title2, true);
+
+ NSButton* first = [[bar_ buttons] objectAtIndex:0];
+ NSButton* second = [[bar_ buttons] objectAtIndex:1];
+ EXPECT_TRUE(first && second);
+
+ NSMenuItem* menuItem = [[[first cell] menu] itemAtIndex:0];
+ const BookmarkNode* node = [bar_ nodeFromMenuItem:menuItem];
+ EXPECT_TRUE(node);
+ EXPECT_EQ(node, model->GetBookmarkBarNode()->GetChild(0));
+
+ menuItem = [[[second cell] menu] itemAtIndex:0];
+ node = [bar_ nodeFromMenuItem:menuItem];
+ EXPECT_TRUE(node);
+ EXPECT_EQ(node, model->GetBookmarkBarNode()->GetChild(1));
+
+ menuItem = [[[bar_ view] menu] itemAtIndex:0];
+ node = [bar_ nodeFromMenuItem:menuItem];
+ EXPECT_TRUE(node);
+ EXPECT_EQ(node, model->GetBookmarkBarNode());
+}
+
+TEST_F(BookmarkBarControllerTest, TestMenuNodeAndDisable) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ const BookmarkNode* folder = model->AddGroup(parent,
+ parent->GetChildCount(),
+ ASCIIToUTF16("group"));
+ NSButton* button = [[bar_ buttons] objectAtIndex:0];
+ EXPECT_TRUE(button);
+
+ // Confirm the menu knows which node it is talking about
+ BookmarkMenu* menu = static_cast<BookmarkMenu*>([[button cell] menu]);
+ EXPECT_TRUE(menu);
+ EXPECT_TRUE([menu isKindOfClass:[BookmarkMenu class]]);
+ EXPECT_EQ(folder->id(), [menu id]);
+
+ // Make sure "Open All" is disabled (nothing to open -- no children!)
+ // (Assumes "Open All" is the 1st item)
+ NSMenuItem* item = [menu itemAtIndex:0];
+ EXPECT_FALSE([bar_ validateUserInterfaceItem:item]);
+
+ // Now add a child and make sure the item would be enabled.
+ model->AddURL(folder, folder->GetChildCount(),
+ ASCIIToUTF16("super duper wide title"),
+ GURL("http://superfriends.hall-of-justice.edu"));
+ EXPECT_TRUE([bar_ validateUserInterfaceItem:item]);
+}
+
+TEST_F(BookmarkBarControllerTest, TestDragButton) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+
+ GURL gurls[] = { GURL("http://www.google.com/a"),
+ GURL("http://www.google.com/b"),
+ GURL("http://www.google.com/c") };
+ string16 titles[] = { ASCIIToUTF16("a"),
+ ASCIIToUTF16("b"),
+ ASCIIToUTF16("c") };
+ for (unsigned i = 0; i < arraysize(titles); i++) {
+ model->SetURLStarred(gurls[i], titles[i], true);
+ }
+
+ EXPECT_EQ([[bar_ buttons] count], arraysize(titles));
+ EXPECT_NSEQ(@"a", [[[bar_ buttons] objectAtIndex:0] title]);
+
+ [bar_ dragButton:[[bar_ buttons] objectAtIndex:2]
+ to:NSMakePoint(0, 0)
+ copy:NO];
+ EXPECT_NSEQ(@"c", [[[bar_ buttons] objectAtIndex:0] title]);
+ // Make sure a 'copy' did not happen.
+ EXPECT_EQ([[bar_ buttons] count], arraysize(titles));
+
+ [bar_ dragButton:[[bar_ buttons] objectAtIndex:1]
+ to:NSMakePoint(1000, 0)
+ copy:NO];
+ EXPECT_NSEQ(@"c", [[[bar_ buttons] objectAtIndex:0] title]);
+ EXPECT_NSEQ(@"b", [[[bar_ buttons] objectAtIndex:1] title]);
+ EXPECT_NSEQ(@"a", [[[bar_ buttons] objectAtIndex:2] title]);
+ EXPECT_EQ([[bar_ buttons] count], arraysize(titles));
+
+ // A drop of the 1st between the next 2.
+ CGFloat x = NSMinX([[[bar_ buttons] objectAtIndex:2] frame]);
+ x += [[bar_ view] frame].origin.x;
+ [bar_ dragButton:[[bar_ buttons] objectAtIndex:0]
+ to:NSMakePoint(x, 0)
+ copy:NO];
+ EXPECT_NSEQ(@"b", [[[bar_ buttons] objectAtIndex:0] title]);
+ EXPECT_NSEQ(@"c", [[[bar_ buttons] objectAtIndex:1] title]);
+ EXPECT_NSEQ(@"a", [[[bar_ buttons] objectAtIndex:2] title]);
+ EXPECT_EQ([[bar_ buttons] count], arraysize(titles));
+
+ // A drop on a non-folder button. (Shouldn't try and go in it.)
+ x = NSMidX([[[bar_ buttons] objectAtIndex:0] frame]);
+ x += [[bar_ view] frame].origin.x;
+ [bar_ dragButton:[[bar_ buttons] objectAtIndex:2]
+ to:NSMakePoint(x, 0)
+ copy:NO];
+ EXPECT_EQ(arraysize(titles), [[bar_ buttons] count]);
+
+ // A drop on a folder button.
+ const BookmarkNode* folder = model->AddGroup(model->GetBookmarkBarNode(),
+ 0,
+ ASCIIToUTF16("awesome group"));
+ DCHECK(folder);
+ model->AddURL(folder, 0, ASCIIToUTF16("already"),
+ GURL("http://www.google.com"));
+ EXPECT_EQ(arraysize(titles) + 1, [[bar_ buttons] count]);
+ EXPECT_EQ(1, folder->GetChildCount());
+ x = NSMidX([[[bar_ buttons] objectAtIndex:0] frame]);
+ x += [[bar_ view] frame].origin.x;
+ string16 title = [[[bar_ buttons] objectAtIndex:2] bookmarkNode]->GetTitle();
+ [bar_ dragButton:[[bar_ buttons] objectAtIndex:2]
+ to:NSMakePoint(x, 0)
+ copy:NO];
+ // Gone from the bar
+ EXPECT_EQ(arraysize(titles), [[bar_ buttons] count]);
+ // In the folder
+ EXPECT_EQ(2, folder->GetChildCount());
+ // At the end
+ EXPECT_EQ(title, folder->GetChild(1)->GetTitle());
+}
+
+TEST_F(BookmarkBarControllerTest, TestCopyButton) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+
+ GURL gurls[] = { GURL("http://www.google.com/a"),
+ GURL("http://www.google.com/b"),
+ GURL("http://www.google.com/c") };
+ string16 titles[] = { ASCIIToUTF16("a"),
+ ASCIIToUTF16("b"),
+ ASCIIToUTF16("c") };
+ for (unsigned i = 0; i < arraysize(titles); i++) {
+ model->SetURLStarred(gurls[i], titles[i], true);
+ }
+ EXPECT_EQ([[bar_ buttons] count], arraysize(titles));
+ EXPECT_NSEQ(@"a", [[[bar_ buttons] objectAtIndex:0] title]);
+
+ // Drag 'a' between 'b' and 'c'.
+ CGFloat x = NSMinX([[[bar_ buttons] objectAtIndex:2] frame]);
+ x += [[bar_ view] frame].origin.x;
+ [bar_ dragButton:[[bar_ buttons] objectAtIndex:0]
+ to:NSMakePoint(x, 0)
+ copy:YES];
+ EXPECT_NSEQ(@"a", [[[bar_ buttons] objectAtIndex:0] title]);
+ EXPECT_NSEQ(@"b", [[[bar_ buttons] objectAtIndex:1] title]);
+ EXPECT_NSEQ(@"a", [[[bar_ buttons] objectAtIndex:2] title]);
+ EXPECT_NSEQ(@"c", [[[bar_ buttons] objectAtIndex:3] title]);
+ EXPECT_EQ([[bar_ buttons] count], 4U);
+}
+
+// Fake a theme with colored text. Apply it and make sure bookmark
+// buttons have the same colored text. Repeat more than once.
+TEST_F(BookmarkBarControllerTest, TestThemedButton) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ model->SetURLStarred(GURL("http://www.foo.com"), ASCIIToUTF16("small"), true);
+ BookmarkButton* button = [[bar_ buttons] objectAtIndex:0];
+ EXPECT_TRUE(button);
+
+ NSArray* colors = [NSArray arrayWithObjects:[NSColor redColor],
+ [NSColor blueColor],
+ nil];
+ for (NSColor* color in colors) {
+ FakeTheme theme(color);
+ [bar_ updateTheme:&theme];
+ NSAttributedString* astr = [button attributedTitle];
+ EXPECT_TRUE(astr);
+ EXPECT_NSEQ(@"small", [astr string]);
+ // Pick a char in the middle to test (index 3)
+ NSDictionary* attributes = [astr attributesAtIndex:3 effectiveRange:NULL];
+ NSColor* newColor =
+ [attributes objectForKey:NSForegroundColorAttributeName];
+ EXPECT_NSEQ(newColor, color);
+ }
+}
+
+// Test that delegates and targets of buttons are cleared on dealloc.
+TEST_F(BookmarkBarControllerTest, TestClearOnDealloc) {
+ // Make some bookmark buttons.
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ GURL gurls[] = { GURL("http://www.foo.com/"),
+ GURL("http://www.bar.com/"),
+ GURL("http://www.baz.com/") };
+ string16 titles[] = { ASCIIToUTF16("a"),
+ ASCIIToUTF16("b"),
+ ASCIIToUTF16("c") };
+ for (size_t i = 0; i < arraysize(titles); i++)
+ model->SetURLStarred(gurls[i], titles[i], true);
+
+ // Get and retain the buttons so we can examine them after dealloc.
+ scoped_nsobject<NSArray> buttons([[bar_ buttons] retain]);
+ EXPECT_EQ([buttons count], arraysize(titles));
+
+ // Make sure that everything is set.
+ for (BookmarkButton* button in buttons.get()) {
+ ASSERT_TRUE([button isKindOfClass:[BookmarkButton class]]);
+ EXPECT_TRUE([button delegate]);
+ EXPECT_TRUE([button target]);
+ EXPECT_TRUE([button action]);
+ }
+
+ // This will dealloc....
+ bar_.reset();
+
+ // Make sure that everything is cleared.
+ for (BookmarkButton* button in buttons.get()) {
+ EXPECT_FALSE([button delegate]);
+ EXPECT_FALSE([button target]);
+ EXPECT_FALSE([button action]);
+ }
+}
+
+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(),
+ ASCIIToUTF16("group"));
+ model->AddURL(folder, folder->GetChildCount(),
+ ASCIIToUTF16("f1"), GURL("http://framma-lamma.com"));
+ folder = model->AddGroup(parent, parent->GetChildCount(),
+ ASCIIToUTF16("empty"));
+
+ EXPECT_EQ([[bar_ buttons] count], 2U);
+
+ // 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]];
+ BookmarkBarFolderController* 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];
+}
+
+// Verify that the folder menu presentation properly tracks mouse movements
+// over the bar. Until there is a click no folder menus should show. After a
+// click on a folder folder menus should show until another click on a folder
+// button, and a click outside the bar and its folder menus.
+TEST_F(BookmarkBarControllerTest, TestFolderButtons) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2b ] 3b 4f:[ 4f1b 4f2b ] ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model and that we do not have a folder controller.
+ std::string actualModelString = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModelString);
+ EXPECT_FALSE([bar_ folderController]);
+
+ // Add a real bookmark so we can click on it.
+ const BookmarkNode* folder = root->GetChild(3);
+ model.AddURL(folder, folder->GetChildCount(), ASCIIToUTF16("CLICK ME"),
+ GURL("http://www.google.com/"));
+
+ // Click on a folder button.
+ BookmarkButton* button = [bar_ buttonWithTitleEqualTo:@"4f"];
+ EXPECT_TRUE(button);
+ [bar_ openBookmarkFolderFromButton:button];
+ BookmarkBarFolderController* bbfc = [bar_ folderController];
+ EXPECT_TRUE(bbfc);
+
+ // Make sure a 2nd click on the same button closes things.
+ [bar_ openBookmarkFolderFromButton:button];
+ EXPECT_FALSE([bar_ folderController]);
+
+ // Next open is a different button.
+ button = [bar_ buttonWithTitleEqualTo:@"2f"];
+ EXPECT_TRUE(button);
+ [bar_ openBookmarkFolderFromButton:button];
+ EXPECT_TRUE([bar_ folderController]);
+
+ // Mouse over a non-folder button and confirm controller has gone away.
+ button = [bar_ buttonWithTitleEqualTo:@"1b"];
+ EXPECT_TRUE(button);
+ NSEvent* event = test_event_utils::MouseEventAtPoint([button center],
+ NSMouseMoved, 0);
+ [bar_ mouseEnteredButton:button event:event];
+ EXPECT_FALSE([bar_ folderController]);
+
+ // Mouse over the original folder and confirm a new controller.
+ button = [bar_ buttonWithTitleEqualTo:@"2f"];
+ EXPECT_TRUE(button);
+ [bar_ mouseEnteredButton:button event:event];
+ BookmarkBarFolderController* oldBBFC = [bar_ folderController];
+ EXPECT_TRUE(oldBBFC);
+
+ // 'Jump' over to a different folder and confirm a new controller.
+ button = [bar_ buttonWithTitleEqualTo:@"4f"];
+ EXPECT_TRUE(button);
+ [bar_ mouseEnteredButton:button event:event];
+ BookmarkBarFolderController* newBBFC = [bar_ folderController];
+ EXPECT_TRUE(newBBFC);
+ EXPECT_NE(oldBBFC, newBBFC);
+
+ // A click on a real bookmark should close and stop tracking the folder menus.
+ BookmarkButton* bookmarkButton = [newBBFC buttonWithTitleEqualTo:@"CLICK ME"];
+ EXPECT_TRUE(bookmarkButton);
+ [newBBFC openBookmark:bookmarkButton];
+ EXPECT_FALSE([bar_ folderController]);
+ [bar_ mouseEnteredButton:button event:event];
+ EXPECT_FALSE([bar_ folderController]);
+}
+
+// Make sure the "off the side" folder looks like a bookmark folder
+// but only contains "off the side" items.
+TEST_F(BookmarkBarControllerTest, OffTheSideFolder) {
+
+ // It starts hidden.
+ EXPECT_TRUE([bar_ offTheSideButtonIsHidden]);
+
+ // Create some buttons.
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ for (int x = 0; x < 30; x++) {
+ model->AddURL(parent, parent->GetChildCount(),
+ ASCIIToUTF16("medium-size-title"),
+ GURL("http://framma-lamma.com"));
+ }
+ // Add a couple more so we can delete one and make sure its button goes away.
+ model->AddURL(parent, parent->GetChildCount(),
+ ASCIIToUTF16("DELETE_ME"), GURL("http://ashton-tate.com"));
+ model->AddURL(parent, parent->GetChildCount(),
+ ASCIIToUTF16("medium-size-title"),
+ GURL("http://framma-lamma.com"));
+
+ // Should no longer be hidden.
+ EXPECT_FALSE([bar_ offTheSideButtonIsHidden]);
+
+ // Open it; make sure we have a folder controller.
+ EXPECT_FALSE([bar_ folderController]);
+ [bar_ openOffTheSideFolderFromButton:[bar_ offTheSideButton]];
+ BookmarkBarFolderController* bbfc = [bar_ folderController];
+ EXPECT_TRUE(bbfc);
+
+ // Confirm the contents are only buttons which fell off the side by
+ // making sure that none of the nodes in the off-the-side folder are
+ // found in bar buttons. Be careful since not all the bar buttons
+ // may be currently displayed.
+ NSArray* folderButtons = [bbfc buttons];
+ NSArray* barButtons = [bar_ buttons];
+ for (BookmarkButton* folderButton in folderButtons) {
+ for (BookmarkButton* barButton in barButtons) {
+ if ([barButton superview]) {
+ EXPECT_NE([folderButton bookmarkNode], [barButton bookmarkNode]);
+ }
+ }
+ }
+
+ // Delete a bookmark in the off-the-side and verify it's gone.
+ BookmarkButton* button = [bbfc buttonWithTitleEqualTo:@"DELETE_ME"];
+ EXPECT_TRUE(button);
+ model->Remove(parent, parent->GetChildCount() - 2);
+ button = [bbfc buttonWithTitleEqualTo:@"DELETE_ME"];
+ EXPECT_FALSE(button);
+}
+
+TEST_F(BookmarkBarControllerTest, EventToExitCheck) {
+ NSEvent* event = test_event_utils::MakeMouseEvent(NSMouseMoved, 0);
+ EXPECT_FALSE([bar_ isEventAnExitEvent: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_ isEventAnExitEvent:event]);
+
+ event = test_event_utils::LeftMouseDownAtPointInWindow(NSMakePoint(100,100),
+ test_window());
+ EXPECT_TRUE([bar_ isEventAnExitEvent:event]);
+
+ // Many components are arbitrary (e.g. location, keycode).
+ event = [NSEvent keyEventWithType:NSKeyDown
+ location:NSMakePoint(1,1)
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:nil
+ characters:@"x"
+ charactersIgnoringModifiers:@"x"
+ isARepeat:NO
+ keyCode:87];
+ EXPECT_TRUE([bar_ isEventAnExitEvent: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(), ASCIIToUTF16("group 1"));
+ model->AddGroup(parent, parent->GetChildCount(), ASCIIToUTF16("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.
+ CGFloat viewFrameXOffset = [[bar_ view] frame].origin.x;
+ for (BookmarkButton* button in [bar_ buttons]) {
+ CGFloat x = NSMidX([button frame]) + viewFrameXOffset;
+ // 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]) + viewFrameXOffset;
+ EXPECT_NE(button,
+ [bar_ buttonForDroppingOnAtPoint:NSMakePoint(x, 9)]);
+ x = NSMaxX([button frame]) + viewFrameXOffset;
+ EXPECT_NE(button,
+ [bar_ buttonForDroppingOnAtPoint:NSMakePoint(x, 11)]);
+ }
+}
+
+TEST_F(BookmarkBarControllerTest, NodeDeletedWhileMenuIsOpen) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ [bar_ loaded:model];
+
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ const BookmarkNode* initialNode = model->AddURL(
+ parent, parent->GetChildCount(),
+ ASCIIToUTF16("initial"),
+ GURL("http://www.google.com"));
+
+ NSMenuItem* item = ItemForBookmarkBarMenu(initialNode);
+ EXPECT_EQ(0U, noOpenBar()->urls_.size());
+
+ // Basic check of the menu item and an IBOutlet it can call.
+ EXPECT_EQ(initialNode, [bar_ nodeFromMenuItem:item]);
+ [bar_ openBookmarkInNewWindow:item];
+ EXPECT_EQ(1U, noOpenBar()->urls_.size());
+ [bar_ clear];
+
+ // Now delete the node and make sure things are happy (no crash,
+ // NULL node caught).
+ model->Remove(parent, parent->IndexOfChild(initialNode));
+ EXPECT_EQ(nil, [bar_ nodeFromMenuItem:item]);
+ // Should not crash by referencing a deleted node.
+ [bar_ openBookmarkInNewWindow:item];
+ // Confirm the above did nothing in case it somehow didn't crash.
+ EXPECT_EQ(0U, noOpenBar()->urls_.size());
+
+ // Confirm some more non-crashes.
+ [bar_ openBookmarkInNewForegroundTab:item];
+ [bar_ openBookmarkInIncognitoWindow:item];
+ [bar_ editBookmark:item];
+ [bar_ copyBookmark:item];
+ [bar_ deleteBookmark:item];
+ [bar_ openAllBookmarks:item];
+ [bar_ openAllBookmarksNewWindow:item];
+ [bar_ openAllBookmarksIncognitoWindow:item];
+}
+
+TEST_F(BookmarkBarControllerTest, NodeDeletedWhileContextMenuIsOpen) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ [bar_ loaded:model];
+
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ const BookmarkNode* folder = model->AddGroup(parent,
+ parent->GetChildCount(),
+ ASCIIToUTF16("group"));
+ const BookmarkNode* framma = model->AddURL(folder, folder->GetChildCount(),
+ ASCIIToUTF16("f1"),
+ GURL("http://framma-lamma.com"));
+
+ // Mock in a menu
+ id origMenu = [bar_ buttonContextMenu];
+ id fakeMenu = [OCMockObject partialMockForObject:origMenu];
+ [[fakeMenu expect] cancelTracking];
+ [bar_ setButtonContextMenu:fakeMenu];
+
+ // Force a delete which should cancelTracking on the menu.
+ model->Remove(framma->GetParent(), framma->GetParent()->IndexOfChild(framma));
+
+ // Restore, then confirm cancelTracking was called.
+ [bar_ setButtonContextMenu:origMenu];
+ [fakeMenu verify];
+}
+
+TEST_F(BookmarkBarControllerTest, CloseFolderOnAnimate) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ const BookmarkNode* folder = model->AddGroup(parent,
+ parent->GetChildCount(),
+ ASCIIToUTF16("group"));
+ model->AddGroup(parent, parent->GetChildCount(),
+ ASCIIToUTF16("sibbling group"));
+ model->AddURL(folder, folder->GetChildCount(), ASCIIToUTF16("title a"),
+ GURL("http://www.google.com/a"));
+ model->AddURL(folder, folder->GetChildCount(),
+ ASCIIToUTF16("title super duper long long whoa momma title you betcha"),
+ GURL("http://www.google.com/b"));
+ BookmarkButton* button = [[bar_ buttons] objectAtIndex:0];
+ EXPECT_FALSE([bar_ folderController]);
+ [bar_ openBookmarkFolderFromButton:button];
+ BookmarkBarFolderController* bbfc = [bar_ folderController];
+ // The following tells us that the folder menu is showing. We want to make
+ // sure the folder menu goes away if the bookmark bar is hidden.
+ EXPECT_TRUE(bbfc);
+ EXPECT_TRUE([bar_ isVisible]);
+
+ // Hide the bookmark bar.
+ [bar_ updateAndShowNormalBar:NO
+ showDetachedBar:YES
+ withAnimation:YES];
+ EXPECT_TRUE([bar_ isAnimationRunning]);
+
+ // Now that we've closed the bookmark bar (with animation) the folder menu
+ // should have been closed thus releasing the folderController.
+ EXPECT_FALSE([bar_ folderController]);
+}
+
+TEST_F(BookmarkBarControllerTest, MoveRemoveAddButtons) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2b ] 3b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actualModelString = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModelString);
+
+ // Remember how many buttons are showing.
+ int oldDisplayedButtons = [bar_ displayedButtonCount];
+ NSArray* buttons = [bar_ buttons];
+
+ // Move a button around a bit.
+ [bar_ moveButtonFromIndex:0 toIndex:2];
+ EXPECT_NSEQ(@"2f", [[buttons objectAtIndex:0] title]);
+ EXPECT_NSEQ(@"3b", [[buttons objectAtIndex:1] title]);
+ EXPECT_NSEQ(@"1b", [[buttons objectAtIndex:2] title]);
+ EXPECT_EQ(oldDisplayedButtons, [bar_ displayedButtonCount]);
+ [bar_ moveButtonFromIndex:2 toIndex:0];
+ EXPECT_NSEQ(@"1b", [[buttons objectAtIndex:0] title]);
+ EXPECT_NSEQ(@"2f", [[buttons objectAtIndex:1] title]);
+ EXPECT_NSEQ(@"3b", [[buttons objectAtIndex:2] title]);
+ EXPECT_EQ(oldDisplayedButtons, [bar_ displayedButtonCount]);
+
+ // Add a couple of buttons.
+ const BookmarkNode* parent = root->GetChild(1); // Purloin an existing node.
+ const BookmarkNode* node = parent->GetChild(0);
+ [bar_ addButtonForNode:node atIndex:0];
+ EXPECT_NSEQ(@"2f1b", [[buttons objectAtIndex:0] title]);
+ EXPECT_NSEQ(@"1b", [[buttons objectAtIndex:1] title]);
+ EXPECT_NSEQ(@"2f", [[buttons objectAtIndex:2] title]);
+ EXPECT_NSEQ(@"3b", [[buttons objectAtIndex:3] title]);
+ EXPECT_EQ(oldDisplayedButtons + 1, [bar_ displayedButtonCount]);
+ node = parent->GetChild(1);
+ [bar_ addButtonForNode:node atIndex:-1];
+ EXPECT_NSEQ(@"2f1b", [[buttons objectAtIndex:0] title]);
+ EXPECT_NSEQ(@"1b", [[buttons objectAtIndex:1] title]);
+ EXPECT_NSEQ(@"2f", [[buttons objectAtIndex:2] title]);
+ EXPECT_NSEQ(@"3b", [[buttons objectAtIndex:3] title]);
+ EXPECT_NSEQ(@"2f2b", [[buttons objectAtIndex:4] title]);
+ EXPECT_EQ(oldDisplayedButtons + 2, [bar_ displayedButtonCount]);
+
+ // Remove a couple of buttons.
+ [bar_ removeButton:4 animate:NO];
+ [bar_ removeButton:1 animate:NO];
+ EXPECT_NSEQ(@"2f1b", [[buttons objectAtIndex:0] title]);
+ EXPECT_NSEQ(@"2f", [[buttons objectAtIndex:1] title]);
+ EXPECT_NSEQ(@"3b", [[buttons objectAtIndex:2] title]);
+ EXPECT_EQ(oldDisplayedButtons, [bar_ displayedButtonCount]);
+}
+
+TEST_F(BookmarkBarControllerTest, ShrinkOrHideView) {
+ NSRect viewFrame = NSMakeRect(0.0, 0.0, 500.0, 50.0);
+ NSView* view = [[[NSView alloc] initWithFrame:viewFrame] autorelease];
+ EXPECT_FALSE([view isHidden]);
+ [bar_ shrinkOrHideView:view forMaxX:500.0];
+ EXPECT_EQ(500.0, NSWidth([view frame]));
+ EXPECT_FALSE([view isHidden]);
+ [bar_ shrinkOrHideView:view forMaxX:450.0];
+ EXPECT_EQ(450.0, NSWidth([view frame]));
+ EXPECT_FALSE([view isHidden]);
+ [bar_ shrinkOrHideView:view forMaxX:40.0];
+ EXPECT_EQ(40.0, NSWidth([view frame]));
+ EXPECT_FALSE([view isHidden]);
+ [bar_ shrinkOrHideView:view forMaxX:31.0];
+ EXPECT_EQ(31.0, NSWidth([view frame]));
+ EXPECT_FALSE([view isHidden]);
+ [bar_ shrinkOrHideView:view forMaxX:29.0];
+ EXPECT_TRUE([view isHidden]);
+}
+
+class BookmarkBarControllerOpenAllTest : public BookmarkBarControllerTest {
+public:
+ BookmarkBarControllerOpenAllTest() {
+ resizeDelegate_.reset([[ViewResizerPong alloc] init]);
+ NSRect parent_frame = NSMakeRect(0, 0, 800, 50);
+ bar_.reset(
+ [[BookmarkBarControllerOpenAllPong alloc]
+ initWithBrowser:helper_.browser()
+ initialWidth:NSWidth(parent_frame)
+ delegate:nil
+ resizeDelegate:resizeDelegate_.get()]);
+ [bar_ view];
+ // Awkwardness to look like we've been installed.
+ [parent_view_ addSubview:[bar_ view]];
+ NSRect frame = [[[bar_ view] superview] frame];
+ frame.origin.y = 100;
+ [[[bar_ view] superview] setFrame:frame];
+
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ parent_ = model->GetBookmarkBarNode();
+ // { one, { two-one, two-two }, three }
+ model->AddURL(parent_, parent_->GetChildCount(), ASCIIToUTF16("title"),
+ GURL("http://one.com"));
+ folder_ = model->AddGroup(parent_, parent_->GetChildCount(),
+ ASCIIToUTF16("group"));
+ model->AddURL(folder_, folder_->GetChildCount(),
+ ASCIIToUTF16("title"), GURL("http://two-one.com"));
+ model->AddURL(folder_, folder_->GetChildCount(),
+ ASCIIToUTF16("title"), GURL("http://two-two.com"));
+ model->AddURL(parent_, parent_->GetChildCount(),
+ ASCIIToUTF16("title"), GURL("https://three.com"));
+ }
+ const BookmarkNode* parent_; // Weak
+ const BookmarkNode* folder_; // Weak
+};
+
+TEST_F(BookmarkBarControllerOpenAllTest, OpenAllBookmarks) {
+ // Our first OpenAll... is from the bar itself.
+ [bar_ openAllBookmarks:ItemForBookmarkBarMenu(parent_)];
+ BookmarkBarControllerOpenAllPong* specialBar =
+ (BookmarkBarControllerOpenAllPong*)bar_.get();
+ EXPECT_EQ([specialBar dispositionDetected], NEW_FOREGROUND_TAB);
+
+ // Now try an OpenAll... from a folder node.
+ [specialBar setDispositionDetected:IGNORE_ACTION]; // Reset
+ [bar_ openAllBookmarks:ItemForBookmarkBarMenu(folder_)];
+ EXPECT_EQ([specialBar dispositionDetected], NEW_FOREGROUND_TAB);
+}
+
+TEST_F(BookmarkBarControllerOpenAllTest, OpenAllNewWindow) {
+ // Our first OpenAll... is from the bar itself.
+ [bar_ openAllBookmarksNewWindow:ItemForBookmarkBarMenu(parent_)];
+ BookmarkBarControllerOpenAllPong* specialBar =
+ (BookmarkBarControllerOpenAllPong*)bar_.get();
+ EXPECT_EQ([specialBar dispositionDetected], NEW_WINDOW);
+
+ // Now try an OpenAll... from a folder node.
+ [specialBar setDispositionDetected:IGNORE_ACTION]; // Reset
+ [bar_ openAllBookmarksNewWindow:ItemForBookmarkBarMenu(folder_)];
+ EXPECT_EQ([specialBar dispositionDetected], NEW_WINDOW);
+}
+
+TEST_F(BookmarkBarControllerOpenAllTest, OpenAllIncognito) {
+ // Our first OpenAll... is from the bar itself.
+ [bar_ openAllBookmarksIncognitoWindow:ItemForBookmarkBarMenu(parent_)];
+ BookmarkBarControllerOpenAllPong* specialBar =
+ (BookmarkBarControllerOpenAllPong*)bar_.get();
+ EXPECT_EQ([specialBar dispositionDetected], OFF_THE_RECORD);
+
+ // Now try an OpenAll... from a folder node.
+ [specialBar setDispositionDetected:IGNORE_ACTION]; // Reset
+ [bar_ openAllBookmarksIncognitoWindow:ItemForBookmarkBarMenu(folder_)];
+ EXPECT_EQ([specialBar dispositionDetected], OFF_THE_RECORD);
+}
+
+// Command-click on a folder should open all the bookmarks in it.
+TEST_F(BookmarkBarControllerOpenAllTest, CommandClickOnFolder) {
+ NSButton* first = [[bar_ buttons] objectAtIndex:0];
+ EXPECT_TRUE(first);
+
+ // Create the right kind of event; mock NSApp so [NSApp
+ // currentEvent] finds it.
+ NSEvent* commandClick = test_event_utils::MouseEventAtPoint(NSZeroPoint,
+ NSLeftMouseDown,
+ NSCommandKeyMask);
+ id fakeApp = [OCMockObject partialMockForObject:NSApp];
+ [[[fakeApp stub] andReturn:commandClick] currentEvent];
+ id oldApp = NSApp;
+ NSApp = fakeApp;
+ size_t originalDispositionCount = noOpenBar()->dispositions_.size();
+
+ // Click!
+ [first performClick:first];
+
+ size_t dispositionCount = noOpenBar()->dispositions_.size();
+ EXPECT_EQ(originalDispositionCount+1, dispositionCount);
+ EXPECT_EQ(noOpenBar()->dispositions_[dispositionCount-1], NEW_BACKGROUND_TAB);
+
+ // Replace NSApp
+ NSApp = oldApp;
+}
+
+class BookmarkBarControllerNotificationTest : public CocoaTest {
+ public:
+ BookmarkBarControllerNotificationTest() {
+ resizeDelegate_.reset([[ViewResizerPong alloc] init]);
+ NSRect parent_frame = NSMakeRect(0, 0, 800, 50);
+ parent_view_.reset([[NSView alloc] initWithFrame:parent_frame]);
+ [parent_view_ setHidden:YES];
+ bar_.reset(
+ [[BookmarkBarControllerNotificationPong alloc]
+ initWithBrowser:helper_.browser()
+ initialWidth:NSWidth(parent_frame)
+ delegate:nil
+ resizeDelegate:resizeDelegate_.get()]);
+
+ // Force loading of the nib.
+ [bar_ view];
+ // Awkwardness to look like we've been installed.
+ [parent_view_ addSubview:[bar_ view]];
+ NSRect frame = [[[bar_ view] superview] frame];
+ frame.origin.y = 100;
+ [[[bar_ view] superview] setFrame:frame];
+
+ // Do not add the bar to a window, yet.
+ }
+
+ BrowserTestHelper helper_;
+ scoped_nsobject<NSView> parent_view_;
+ scoped_nsobject<ViewResizerPong> resizeDelegate_;
+ scoped_nsobject<BookmarkBarControllerNotificationPong> bar_;
+};
+
+TEST_F(BookmarkBarControllerNotificationTest, DeregistersForNotifications) {
+ NSWindow* window = [[CocoaTestHelperWindow alloc] init];
+ [window setReleasedWhenClosed:YES];
+
+ // First add the bookmark bar to the temp window, then to another window.
+ [[window contentView] addSubview:parent_view_];
+ [[test_window() contentView] addSubview:parent_view_];
+
+ // Post a fake windowDidResignKey notification for the temp window and make
+ // sure the bookmark bar controller wasn't listening.
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:NSWindowDidResignKeyNotification
+ object:window];
+ EXPECT_FALSE([bar_ windowDidResignKeyReceived]);
+
+ // Close the temp window and make sure no notification was received.
+ [window close];
+ EXPECT_FALSE([bar_ windowWillCloseReceived]);
+}
+
+
+// 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.
+
+class BookmarkBarControllerDragDropTest : public BookmarkBarControllerTestBase {
+ public:
+ scoped_nsobject<BookmarkBarControllerDragData> bar_;
+
+ BookmarkBarControllerDragDropTest() {
+ bar_.reset(
+ [[BookmarkBarControllerDragData alloc]
+ initWithBrowser:helper_.browser()
+ initialWidth:NSWidth([parent_view_ frame])
+ delegate:nil
+ resizeDelegate:resizeDelegate_.get()]);
+ InstallAndToggleBar(bar_.get());
+ }
+};
+
+TEST_F(BookmarkBarControllerDragDropTest, DragMoveBarBookmarkToOffTheSide) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1bWithLongName 2fWithLongName:[ "
+ "2f1bWithLongName 2f2fWithLongName:[ 2f2f1bWithLongName "
+ "2f2f2bWithLongName 2f2f3bWithLongName 2f4b ] 2f3bWithLongName ] "
+ "3bWithLongName 4bWithLongName 5bWithLongName 6bWithLongName "
+ "7bWithLongName 8bWithLongName 9bWithLongName 10bWithLongName "
+ "11bWithLongName 12bWithLongName 13b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actualModelString = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModelString);
+
+ // Insure that the off-the-side is not showing.
+ ASSERT_FALSE([bar_ offTheSideButtonIsHidden]);
+
+ // Remember how many buttons are showing and are available.
+ int oldDisplayedButtons = [bar_ displayedButtonCount];
+ int oldChildCount = root->GetChildCount();
+
+ // Pop up the off-the-side menu.
+ BookmarkButton* otsButton = (BookmarkButton*)[bar_ offTheSideButton];
+ ASSERT_TRUE(otsButton);
+ [[otsButton target] performSelector:@selector(openOffTheSideFolderFromButton:)
+ withObject:otsButton];
+ BookmarkBarFolderController* otsController = [bar_ folderController];
+ EXPECT_TRUE(otsController);
+ NSWindow* toWindow = [otsController window];
+ EXPECT_TRUE(toWindow);
+ BookmarkButton* draggedButton =
+ [bar_ buttonWithTitleEqualTo:@"3bWithLongName"];
+ ASSERT_TRUE(draggedButton);
+ int oldOTSCount = (int)[[otsController buttons] count];
+ EXPECT_EQ(oldOTSCount, oldChildCount - oldDisplayedButtons);
+ BookmarkButton* targetButton = [[otsController buttons] objectAtIndex:0];
+ ASSERT_TRUE(targetButton);
+ [otsController dragButton:draggedButton
+ to:[targetButton center]
+ copy:YES];
+ // There should still be the same number of buttons in the bar
+ // and off-the-side should have one more.
+ int newDisplayedButtons = [bar_ displayedButtonCount];
+ int newChildCount = root->GetChildCount();
+ int newOTSCount = (int)[[otsController buttons] count];
+ EXPECT_EQ(oldDisplayedButtons, newDisplayedButtons);
+ EXPECT_EQ(oldChildCount + 1, newChildCount);
+ EXPECT_EQ(oldOTSCount + 1, newOTSCount);
+ EXPECT_EQ(newOTSCount, newChildCount - newDisplayedButtons);
+}
+
+TEST_F(BookmarkBarControllerDragDropTest, DragOffTheSideToOther) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1bWithLongName 2bWithLongName "
+ "3bWithLongName 4bWithLongName 5bWithLongName 6bWithLongName "
+ "7bWithLongName 8bWithLongName 9bWithLongName 10bWithLongName "
+ "11bWithLongName 12bWithLongName 13bWithLongName 14bWithLongName "
+ "15bWithLongName 16bWithLongName 17bWithLongName 18bWithLongName "
+ "19bWithLongName 20bWithLongName ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ const BookmarkNode* other = model.other_node();
+ const std::string other_string("1other 2other 3other ");
+ model_test_utils::AddNodesFromModelString(model, other, other_string);
+
+ // Validate initial model.
+ std::string actualModelString = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModelString);
+ std::string actualOtherString = model_test_utils::ModelStringFromNode(other);
+ EXPECT_EQ(other_string, actualOtherString);
+
+ // Insure that the off-the-side is showing.
+ ASSERT_FALSE([bar_ offTheSideButtonIsHidden]);
+
+ // Remember how many buttons are showing and are available.
+ int oldDisplayedButtons = [bar_ displayedButtonCount];
+ int oldRootCount = root->GetChildCount();
+ int oldOtherCount = other->GetChildCount();
+
+ // Pop up the off-the-side menu.
+ BookmarkButton* otsButton = (BookmarkButton*)[bar_ offTheSideButton];
+ ASSERT_TRUE(otsButton);
+ [[otsButton target] performSelector:@selector(openOffTheSideFolderFromButton:)
+ withObject:otsButton];
+ BookmarkBarFolderController* otsController = [bar_ folderController];
+ EXPECT_TRUE(otsController);
+ int oldOTSCount = (int)[[otsController buttons] count];
+ EXPECT_EQ(oldOTSCount, oldRootCount - oldDisplayedButtons);
+
+ // Pick an off-the-side button and drag it to the other bookmarks.
+ BookmarkButton* draggedButton =
+ [otsController buttonWithTitleEqualTo:@"20bWithLongName"];
+ ASSERT_TRUE(draggedButton);
+ BookmarkButton* targetButton = [bar_ otherBookmarksButton];
+ ASSERT_TRUE(targetButton);
+ [bar_ dragButton:draggedButton to:[targetButton center] copy:NO];
+
+ // There should one less button in the bar, one less in off-the-side,
+ // and one more in other bookmarks.
+ int newRootCount = root->GetChildCount();
+ int newOTSCount = (int)[[otsController buttons] count];
+ int newOtherCount = other->GetChildCount();
+ EXPECT_EQ(oldRootCount - 1, newRootCount);
+ EXPECT_EQ(oldOTSCount - 1, newOTSCount);
+ EXPECT_EQ(oldOtherCount + 1, newOtherCount);
+}
+
+TEST_F(BookmarkBarControllerDragDropTest, DragBookmarkData) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] "
+ "2f3b ] 3b 4b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+ const BookmarkNode* other = model.other_node();
+ const std::string other_string("O1b O2b O3f:[ O3f1b O3f2f ] "
+ "O4f:[ O4f1b O4f2f ] 05b ");
+ model_test_utils::AddNodesFromModelString(model, other, other_string);
+
+ // Validate initial model.
+ std::string actual = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actual);
+ actual = model_test_utils::ModelStringFromNode(other);
+ EXPECT_EQ(other_string, actual);
+
+ // Remember the little ones.
+ int oldChildCount = root->GetChildCount();
+
+ BookmarkButton* targetButton = [bar_ buttonWithTitleEqualTo:@"3b"];
+ ASSERT_TRUE(targetButton);
+
+ // Gen up some dragging data.
+ const BookmarkNode* newNode = other->GetChild(2);
+ [bar_ setDragDataNode:newNode];
+ scoped_nsobject<FakeDragInfo> dragInfo([[FakeDragInfo alloc] init]);
+ [dragInfo setDropLocation:[targetButton center]];
+ [bar_ dragBookmarkData:(id<NSDraggingInfo>)dragInfo.get()];
+
+ // There should one more button in the bar.
+ int newChildCount = root->GetChildCount();
+ EXPECT_EQ(oldChildCount + 1, newChildCount);
+ // Verify the model.
+ const std::string expected("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] "
+ "2f3b ] O3f:[ O3f1b O3f2f ] 3b 4b ");
+ actual = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(expected, actual);
+ oldChildCount = newChildCount;
+
+ // Now do it over a folder button.
+ targetButton = [bar_ buttonWithTitleEqualTo:@"2f"];
+ ASSERT_TRUE(targetButton);
+ NSPoint targetPoint = [targetButton center];
+ newNode = other->GetChild(2); // Should be O4f.
+ EXPECT_EQ(newNode->GetTitle(), ASCIIToUTF16("O4f"));
+ [bar_ setDragDataNode:newNode];
+ [dragInfo setDropLocation:targetPoint];
+ [bar_ dragBookmarkData:(id<NSDraggingInfo>)dragInfo.get()];
+
+ newChildCount = root->GetChildCount();
+ EXPECT_EQ(oldChildCount, newChildCount);
+ // Verify the model.
+ const std::string expected1("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] "
+ "2f3b O4f:[ O4f1b O4f2f ] ] O3f:[ O3f1b O3f2f ] "
+ "3b 4b ");
+ actual = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(expected1, actual);
+}
+
+TEST_F(BookmarkBarControllerDragDropTest, AddURLs) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] "
+ "2f3b ] 3b 4b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actual = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actual);
+
+ // Remember the children.
+ int oldChildCount = root->GetChildCount();
+
+ BookmarkButton* targetButton = [bar_ buttonWithTitleEqualTo:@"3b"];
+ ASSERT_TRUE(targetButton);
+
+ NSArray* urls = [NSArray arrayWithObjects: @"http://www.a.com/",
+ @"http://www.b.com/", nil];
+ NSArray* titles = [NSArray arrayWithObjects: @"SiteA", @"SiteB", nil];
+ [bar_ addURLs:urls withTitles:titles at:[targetButton center]];
+
+ // There should two more nodes in the bar.
+ int newChildCount = root->GetChildCount();
+ EXPECT_EQ(oldChildCount + 2, newChildCount);
+ // Verify the model.
+ const std::string expected("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] "
+ "2f3b ] SiteA SiteB 3b 4b ");
+ actual = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(expected, actual);
+}
+
+TEST_F(BookmarkBarControllerDragDropTest, ControllerForNode) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2b ] 3b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actualModelString = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModelString);
+
+ // Find the main bar controller.
+ const void* expectedController = bar_;
+ const void* actualController = [bar_ controllerForNode:root];
+ EXPECT_EQ(expectedController, actualController);
+}
+
+TEST_F(BookmarkBarControllerDragDropTest, DropPositionIndicator) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2b 2f3b ] 3b 4b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actualModel = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModel);
+
+ // Test a series of points starting at the right edge of the bar.
+ BookmarkButton* targetButton = [bar_ buttonWithTitleEqualTo:@"1b"];
+ ASSERT_TRUE(targetButton);
+ NSPoint targetPoint = [targetButton left];
+ const CGFloat xDelta = 0.5 * bookmarks::kBookmarkHorizontalPadding;
+ const CGFloat baseOffset = targetPoint.x;
+ CGFloat expected = xDelta;
+ CGFloat actual = [bar_ indicatorPosForDragToPoint:targetPoint];
+ EXPECT_CGFLOAT_EQ(expected, actual);
+ targetButton = [bar_ buttonWithTitleEqualTo:@"2f"];
+ actual = [bar_ indicatorPosForDragToPoint:[targetButton right]];
+ targetButton = [bar_ buttonWithTitleEqualTo:@"3b"];
+ expected = [targetButton left].x - baseOffset + xDelta;
+ EXPECT_CGFLOAT_EQ(expected, actual);
+ targetButton = [bar_ buttonWithTitleEqualTo:@"4b"];
+ targetPoint = [targetButton right];
+ targetPoint.x += 100; // Somewhere off to the right.
+ expected = NSMaxX([targetButton frame]) + xDelta;
+ actual = [bar_ indicatorPosForDragToPoint:targetPoint];
+ EXPECT_CGFLOAT_EQ(expected, actual);
+}
+
+TEST_F(BookmarkBarControllerDragDropTest, PulseButton) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* root = model->GetBookmarkBarNode();
+ GURL gurl("http://www.google.com");
+ const BookmarkNode* node = model->AddURL(root, root->GetChildCount(),
+ ASCIIToUTF16("title"), gurl);
+
+ BookmarkButton* button = [[bar_ buttons] objectAtIndex:0];
+ EXPECT_FALSE([button isContinuousPulsing]);
+
+ NSValue *value = [NSValue valueWithPointer:node];
+ NSDictionary *dict = [NSDictionary
+ dictionaryWithObjectsAndKeys:value,
+ bookmark_button::kBookmarkKey,
+ [NSNumber numberWithBool:YES],
+ bookmark_button::kBookmarkPulseFlagKey,
+ nil];
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:bookmark_button::kPulseBookmarkButtonNotification
+ object:nil
+ userInfo:dict];
+ EXPECT_TRUE([button isContinuousPulsing]);
+
+ dict = [NSDictionary dictionaryWithObjectsAndKeys:value,
+ bookmark_button::kBookmarkKey,
+ [NSNumber numberWithBool:NO],
+ bookmark_button::kBookmarkPulseFlagKey,
+ nil];
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:bookmark_button::kPulseBookmarkButtonNotification
+ object:nil
+ userInfo:dict];
+ EXPECT_FALSE([button isContinuousPulsing]);
+}
+
+TEST_F(BookmarkBarControllerDragDropTest, DragBookmarkDataToTrash) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] "
+ "2f3b ] 3b 4b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actual = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actual);
+
+ int oldChildCount = root->GetChildCount();
+
+ // Drag a button to the trash.
+ BookmarkButton* buttonToDelete = [bar_ buttonWithTitleEqualTo:@"3b"];
+ ASSERT_TRUE(buttonToDelete);
+ EXPECT_TRUE([bar_ canDragBookmarkButtonToTrash:buttonToDelete]);
+ [bar_ didDragBookmarkToTrash:buttonToDelete];
+
+ // There should be one less button in the bar.
+ int newChildCount = root->GetChildCount();
+ EXPECT_EQ(oldChildCount - 1, newChildCount);
+ // Verify the model.
+ const std::string expected("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] "
+ "2f3b ] 4b ");
+ actual = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(expected, actual);
+
+ // Verify that the other bookmark folder can't be deleted.
+ BookmarkButton *otherButton = [bar_ otherBookmarksButton];
+ EXPECT_FALSE([bar_ canDragBookmarkButtonToTrash:otherButton]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h
new file mode 100644
index 0000000..f599e0a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h
@@ -0,0 +1,31 @@
+// 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_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_FOLDER_BUTTON_CELL_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_FOLDER_BUTTON_CELL_H_
+#pragma once
+
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h"
+
+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 BookmarkBarFolderButtonCell : BookmarkButtonCell {
+ @private
+ scoped_nsobject<NSColor> frameColor_;
+}
+
+// Create a button cell which draws without a theme and with a frame
+// color provided by the BrowserThemeProvider defaults.
++ (id)buttonCellForNode:(const BookmarkNode*)node
+ contextMenu:(NSMenu*)contextMenu
+ cellText:(NSString*)cellText
+ cellImage:(NSImage*)cellImage;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_FOLDER_BUTTON_CELL_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.mm
new file mode 100644
index 0000000..c03500d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.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.
+
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h"
+
+@implementation BookmarkBarFolderButtonCell
+
++ (id)buttonCellForNode:(const BookmarkNode*)node
+ contextMenu:(NSMenu*)contextMenu
+ cellText:(NSString*)cellText
+ cellImage:(NSImage*)cellImage {
+ id buttonCell =
+ [[[BookmarkBarFolderButtonCell alloc] initForNode:node
+ contextMenu:contextMenu
+ cellText:cellText
+ cellImage:cellImage]
+ autorelease];
+ return buttonCell;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell_unittest.mm
new file mode 100644
index 0000000..3c20cf7
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell_unittest.mm
@@ -0,0 +1,24 @@
+// 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/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+
+namespace {
+
+class BookmarkBarFolderButtonCellTest : public CocoaTest {
+};
+
+// Basic creation.
+TEST_F(BookmarkBarFolderButtonCellTest, Create) {
+ scoped_nsobject<BookmarkBarFolderButtonCell> cell;
+ cell.reset([[BookmarkBarFolderButtonCell buttonCellForNode:nil
+ contextMenu:nil
+ cellText:nil
+ cellImage:nil] retain]);
+ EXPECT_TRUE(cell);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h
new file mode 100644
index 0000000..083efac
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h
@@ -0,0 +1,182 @@
+// 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/ui/cocoa/bookmarks/bookmark_button.h"
+
+@class BookmarkBarController;
+@class BookmarkBarFolderView;
+@class BookmarkFolderTarget;
+@class BookmarkBarFolderHoverState;
+
+// 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, if we are a nested folder, otherwise nil.
+ // Strong to insure the object lives as long as we need it.
+ scoped_nsobject<BookmarkBarFolderController> parentController_;
+
+ // The main bar controller from whence we or a parent sprang.
+ BookmarkBarController* barController_; // WEAK: It owns us.
+
+ // Our buttons. We do not have buttons for nested folders.
+ scoped_nsobject<NSMutableArray> buttons_;
+
+ // The scroll view that contains our main button view (below).
+ IBOutlet NSScrollView* scrollView_;
+
+ // Are we scrollable? If no, the full contents of the folder are
+ // always visible.
+ BOOL scrollable_;
+
+ BOOL scrollUpArrowShown_;
+ BOOL scrollDownArrowShown_;
+
+ // YES if subfolders should grow to the right (the default).
+ // Direction switches if we'd grow off the screen.
+ BOOL subFolderGrowthToRight_;
+
+ // The main view of this window (where the buttons go).
+ IBOutlet BookmarkBarFolderView* mainView_;
+
+ // Weak; we keep track to work around a
+ // setShowsBorderOnlyWhileMouseInside bug.
+ BookmarkButton* buttonThatMouseIsIn_;
+
+ // The context menu for a bookmark button which represents an URL.
+ IBOutlet NSMenu* buttonMenu_;
+
+ // The context menu for a bookmark button which represents a folder.
+ IBOutlet NSMenu* folderMenu_;
+
+ // We model hover state as a state machine with specific allowable
+ // transitions. |hoverState_| is the state of this machine at any
+ // given time.
+ scoped_nsobject<BookmarkBarFolderHoverState> hoverState_;
+
+ // Logic for dealing with a click on a bookmark folder button.
+ scoped_nsobject<BookmarkFolderTarget> folderTarget_;
+
+ // 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_;
+
+ // Implement basic menu scrolling through this tracking area.
+ scoped_nsobject<NSTrackingArea> scrollTrackingArea_;
+
+ // Timer to continue scrolling as needed. We own the timer but
+ // don't release it when done (we invalidate it).
+ NSTimer* scrollTimer_;
+
+ // Amount to scroll by on each timer fire. Can be + or -.
+ CGFloat verticalScrollDelta_;
+
+ // We need to know the size of the vertical scrolling arrows so we
+ // can obscure/unobscure them.
+ CGFloat verticalScrollArrowHeight_;
+
+ // Set to YES to prevent any node animations. Useful for unit testing so that
+ // incomplete animations do not cause valgrind complaints.
+ BOOL ignoreAnimations_;
+}
+
+// Designated initializer.
+- (id)initWithParentButton:(BookmarkButton*)button
+ parentController:(BookmarkBarFolderController*)parentController
+ barController:(BookmarkBarController*)barController;
+
+// Return the parent button that owns the bookmark folder we represent.
+- (BookmarkButton*)parentButton;
+
+// Offset our folder menu window. This is usually needed in response to a
+// parent folder menu window or the bookmark bar changing position due to
+// the dragging of a bookmark node from the parent into this folder menu.
+- (void)offsetFolderMenuWindow:(NSSize)offset;
+
+// Re-layout the window menu in case some buttons were added or removed,
+// specifically as a result of the bookmark bar changing configuration
+// and altering the contents of the off-the-side folder.
+- (void)reconfigureMenu;
+
+// Actions from a context menu over a button or folder.
+- (IBAction)cutBookmark:(id)sender;
+- (IBAction)copyBookmark:(id)sender;
+- (IBAction)pasteBookmark:(id)sender;
+- (IBAction)deleteBookmark:(id)sender;
+
+// Passed up by a child view to tell us of a desire to scroll.
+- (void)scrollWheel:(NSEvent *)theEvent;
+
+// Forwarded to the associated BookmarkBarController.
+- (IBAction)addFolder:(id)sender;
+- (IBAction)addPage:(id)sender;
+- (IBAction)editBookmark:(id)sender;
+- (IBAction)openBookmark:(id)sender;
+- (IBAction)openAllBookmarks:(id)sender;
+- (IBAction)openAllBookmarksIncognitoWindow:(id)sender;
+- (IBAction)openAllBookmarksNewWindow:(id)sender;
+- (IBAction)openBookmarkInIncognitoWindow:(id)sender;
+- (IBAction)openBookmarkInNewForegroundTab:(id)sender;
+- (IBAction)openBookmarkInNewWindow:(id)sender;
+
+@property (assign, nonatomic) BOOL subFolderGrowthToRight;
+
+@end
+
+@interface BookmarkBarFolderController(TestingAPI)
+- (NSView*)mainView;
+- (NSPoint)windowTopLeftForWidth:(int)windowWidth;
+- (NSArray*)buttons;
+- (BookmarkBarFolderController*)folderController;
+- (id)folderTarget;
+- (void)configureWindowLevel;
+- (void)performOneScroll:(CGFloat)delta;
+- (BookmarkButton*)buttonThatMouseIsIn;
+// Set to YES in order to prevent animations.
+- (void)setIgnoreAnimations:(BOOL)ignore;
+
+// Return YES if we can scroll up or down.
+- (BOOL)canScrollUp;
+- (BOOL)canScrollDown;
+// Return YES if the scrollable_ flag has been set.
+- (BOOL)scrollable;
+
+- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point;
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.mm
new file mode 100644
index 0000000..7720c90
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.mm
@@ -0,0 +1,1459 @@
+// 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/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h"
+
+#include "base/mac_util.h"
+#include "base/nsimage_cache_mac.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/bookmarks/bookmark_utils.h"
+#import "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/event_utils.h"
+
+namespace {
+
+// Frequency of the scrolling timer in seconds.
+const NSTimeInterval kBookmarkBarFolderScrollInterval = 0.1;
+
+// Amount to scroll by per timer fire. We scroll rather slowly; to
+// accomodate we do several at a time.
+const CGFloat kBookmarkBarFolderScrollAmount =
+ 3 * bookmarks::kBookmarkButtonVerticalSpan;
+
+// Amount to scroll for each scroll wheel delta.
+const CGFloat kBookmarkBarFolderScrollWheelAmount =
+ 1 * bookmarks::kBookmarkButtonVerticalSpan;
+
+// When constraining a scrolling bookmark bar folder window to the
+// screen, shrink the "constrain" by this much vertically. Currently
+// this is 0.0 to avoid a problem with tracking areas leaving the
+// window, but should probably be 8.0 or something.
+// TODO(jrg): http://crbug.com/36225
+const CGFloat kScrollWindowVerticalMargin = 0.0;
+
+} // namespace
+
+@interface BookmarkBarFolderController(Private)
+- (void)configureWindow;
+- (void)addOrUpdateScrollTracking;
+- (void)removeScrollTracking;
+- (void)endScroll;
+- (void)addScrollTimerWithDelta:(CGFloat)delta;
+
+// Determine the best button width (which will be the widest button or the
+// maximum allowable button width, whichever is less) and resize all buttons.
+// Return the new width (so that the window can be adjusted, if necessary).
+- (CGFloat)adjustButtonWidths;
+
+// Returns the total menu height needed to display |buttonCount| buttons.
+// Does not do any fancy tricks like trimming the height to fit on the screen.
+- (int)windowHeightForButtonCount:(int)buttonCount;
+
+// Adjust the height and horizontal position of the window such that the
+// scroll arrows are shown as needed and the window appears completely
+// on screen.
+- (void)adjustWindowForHeight:(int)windowHeight;
+
+// Show or hide the scroll arrows at the top/bottom of the window.
+- (void)showOrHideScrollArrows;
+
+// |point| is in the base coordinate system of the destination window;
+// it comes from an id<NSDraggingInfo>. |copy| is YES if a copy is to be
+// made and inserted into the new location while leaving the bookmark in
+// the old location, otherwise move the bookmark by removing from its old
+// location and inserting into the new location.
+- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
+ to:(NSPoint)point
+ copy:(BOOL)copy;
+
+@end
+
+@interface BookmarkButton (BookmarkBarFolderMenuHighlighting)
+
+// Make the button's border frame always appear when |forceOn| is YES,
+// otherwise only border the button when the mouse is inside the button.
+- (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn;
+
+// On 10.6 event dispatch for an NSButtonCell's
+// showsBorderOnlyWhileMouseInside seems broken if scrolling the
+// view that contains the button. It appears that a mouseExited:
+// gets lost, so the button stays highlit forever. We accomodate
+// here.
+- (void)toggleButtonBorderingWhileMouseInside;
+@end
+
+@implementation BookmarkButton (BookmarkBarFolderMenuHighlighting)
+
+- (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn {
+ [self setShowsBorderOnlyWhileMouseInside:!forceOn];
+ [self setNeedsDisplay];
+}
+
+- (void)toggleButtonBorderingWhileMouseInside {
+ BOOL toggle = [self showsBorderOnlyWhileMouseInside];
+ [self setShowsBorderOnlyWhileMouseInside:!toggle];
+ [self setShowsBorderOnlyWhileMouseInside:toggle];
+}
+
+@end
+
+@implementation BookmarkBarFolderController
+
+@synthesize subFolderGrowthToRight = subFolderGrowthToRight_;
+
+- (id)initWithParentButton:(BookmarkButton*)button
+ parentController:(BookmarkBarFolderController*)parentController
+ barController:(BookmarkBarController*)barController {
+ NSString* nibPath =
+ [mac_util::MainAppBundle() pathForResource:@"BookmarkBarFolderWindow"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
+ parentButton_.reset([button retain]);
+
+ // We want the button to remain bordered as part of the menu path.
+ [button forceButtonBorderToStayOnAlways:YES];
+
+ parentController_.reset([parentController retain]);
+ if (!parentController_)
+ [self setSubFolderGrowthToRight:YES];
+ else
+ [self setSubFolderGrowthToRight:[parentController
+ subFolderGrowthToRight]];
+ barController_ = barController; // WEAK
+ buttons_.reset([[NSMutableArray alloc] init]);
+ folderTarget_.reset([[BookmarkFolderTarget alloc] initWithController:self]);
+ NSImage* image = nsimage_cache::ImageNamed(@"menu_overflow_up.pdf");
+ DCHECK(image);
+ verticalScrollArrowHeight_ = [image size].height;
+ [self configureWindow];
+ hoverState_.reset([[BookmarkBarFolderHoverState alloc] init]);
+ }
+ return self;
+}
+
+- (void)dealloc {
+ // The button is no longer part of the menu path.
+ [parentButton_ forceButtonBorderToStayOnAlways:NO];
+ [parentButton_ setNeedsDisplay];
+
+ [self removeScrollTracking];
+ [self endScroll];
+ [hoverState_ draggingExited];
+
+ // Delegate pattern does not retain; make sure pointers to us are removed.
+ for (BookmarkButton* button in buttons_.get()) {
+ [button setDelegate:nil];
+ [button setTarget:nil];
+ [button setAction:nil];
+ }
+
+ // Note: we don't need to
+ // [NSObject cancelPreviousPerformRequestsWithTarget:self];
+ // Because all of our performSelector: calls use withDelay: which
+ // retains us.
+ [super dealloc];
+}
+
+// Overriden from NSWindowController to call childFolderWillShow: before showing
+// the window.
+- (void)showWindow:(id)sender {
+ [barController_ childFolderWillShow:self];
+ [super showWindow:sender];
+}
+
+- (BookmarkButton*)parentButton {
+ return parentButton_.get();
+}
+
+- (void)offsetFolderMenuWindow:(NSSize)offset {
+ NSWindow* window = [self window];
+ NSRect windowFrame = [window frame];
+ windowFrame.origin.x -= offset.width;
+ windowFrame.origin.y += offset.height; // Yes, in the opposite direction!
+ [window setFrame:windowFrame display:YES];
+ [folderController_ offsetFolderMenuWindow:offset];
+}
+
+- (void)reconfigureMenu {
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+ for (BookmarkButton* button in buttons_.get()) {
+ [button setDelegate:nil];
+ [button removeFromSuperview];
+ }
+ [buttons_ removeAllObjects];
+ [self configureWindow];
+}
+
+#pragma mark Private Methods
+
+- (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)child {
+ NSImage* image = child ? [barController_ favIconForNode:child] : nil;
+ NSMenu* menu = child ? child->is_folder() ? folderMenu_ : buttonMenu_ : nil;
+ BookmarkBarFolderButtonCell* cell =
+ [BookmarkBarFolderButtonCell buttonCellForNode:child
+ contextMenu:menu
+ cellText:nil
+ cellImage:image];
+ [cell setTag:kStandardButtonTypeWithLimitedClickFeedback];
+ return cell;
+}
+
+// Redirect to our logic shared with BookmarkBarController.
+- (IBAction)openBookmarkFolderFromButton:(id)sender {
+ [folderTarget_ openBookmarkFolderFromButton:sender];
+}
+
+// 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.
+// Adjusts the input frame width as appropriate.
+//
+// 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 {
+ BookmarkButtonCell* cell = [self cellForBookmarkNode:node];
+ DCHECK(cell);
+
+ // We must decide if we draw the folder arrow before we ask the cell
+ // how big it needs to be.
+ if (node && node->is_folder()) {
+ // Warning when combining code with bookmark_bar_controller.mm:
+ // this call should NOT be made for the bar buttons; only for the
+ // subfolder buttons.
+ [cell setDrawFolderArrow:YES];
+ }
+
+ // The "+2" is needed because, sometimes, Cocoa is off by a tad when
+ // returning the value it thinks it needs.
+ CGFloat desired = [cell cellSize].width + 2;
+ // The width is determined from the maximum of the proposed width
+ // (provided in |frame|) or the natural width of the title, then
+ // limited by the abolute minimum and maximum allowable widths.
+ frame.size.width =
+ std::min(std::max(bookmarks::kBookmarkMenuButtonMinimumWidth,
+ std::max(frame.size.width, desired)),
+ bookmarks::kBookmarkMenuButtonMaximumWidth);
+
+ BookmarkButton* button = [[[BookmarkButton alloc] initWithFrame:frame]
+ autorelease];
+ DCHECK(button);
+
+ [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::SysUTF16ToNSString(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];
+ [button setBordered:NO];
+ }
+ return button;
+}
+
+// Exposed for testing.
+- (NSView*)mainView {
+ return mainView_;
+}
+
+- (id)folderTarget {
+ return folderTarget_.get();
+}
+
+
+// Our parent controller is another BookmarkBarFolderController, so
+// our window is to the right or left of it. We use a little overlap
+// since it looks much more menu-like than with none. If we would
+// grow off the screen, switch growth to the other direction. Growth
+// direction sticks for folder windows which are descendents of us.
+// If we have tried both directions and neither fits, degrade to a
+// default.
+- (CGFloat)childFolderWindowLeftForWidth:(int)windowWidth {
+ // We may legitimately need to try two times (growth to right and
+ // left but not in that order). Limit us to three tries in case
+ // the folder window can't fit on either side of the screen; we
+ // don't want to loop forever.
+ CGFloat x;
+ int tries = 0;
+ while (tries < 2) {
+ // Try to grow right.
+ if ([self subFolderGrowthToRight]) {
+ tries++;
+ x = NSMaxX([[parentButton_ window] frame]) -
+ bookmarks::kBookmarkMenuOverlap;
+ // If off the screen, switch direction.
+ if ((x + windowWidth +
+ bookmarks::kBookmarkHorizontalScreenPadding) >
+ NSMaxX([[[self window] screen] frame])) {
+ [self setSubFolderGrowthToRight:NO];
+ } else {
+ return x;
+ }
+ }
+ // Try to grow left.
+ if (![self subFolderGrowthToRight]) {
+ tries++;
+ x = NSMinX([[parentButton_ window] frame]) +
+ bookmarks::kBookmarkMenuOverlap -
+ windowWidth;
+ // If off the screen, switch direction.
+ if (x < NSMinX([[[self window] screen] frame])) {
+ [self setSubFolderGrowthToRight:YES];
+ } else {
+ return x;
+ }
+ }
+ }
+ // Unhappy; do the best we can.
+ return NSMaxX([[[self window] screen] frame]) - windowWidth;
+}
+
+
+// Compute and return the top left point of our window (screen
+// coordinates). The top left is positioned in a manner similar to
+// cascading menus. Windows may grow to either the right or left of
+// their parent (if a sub-folder) so we need to know |windowWidth|.
+- (NSPoint)windowTopLeftForWidth:(int)windowWidth {
+ 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_
+ convertPoint:NSZeroPoint toView:nil]];
+ NSPoint bookmarkBarBottomLeftInScreen =
+ [[parentButton_ window]
+ convertBaseToScreen:[[parentButton_ superview]
+ convertPoint:NSZeroPoint toView:nil]];
+ newWindowTopLeft = NSMakePoint(buttonBottomLeftInScreen.x,
+ bookmarkBarBottomLeftInScreen.y);
+ // Make sure the window is on-screen; if not, push left. It is
+ // intentional that top level folders "push left" slightly
+ // different than subfolders.
+ NSRect screenFrame = [[[parentButton_ window] screen] frame];
+ CGFloat spillOff = (newWindowTopLeft.x + windowWidth) - NSMaxX(screenFrame);
+ if (spillOff > 0.0) {
+ newWindowTopLeft.x = std::max(newWindowTopLeft.x - spillOff,
+ NSMinX(screenFrame));
+ }
+ } else {
+ // Parent is a folder; grow right/left.
+ newWindowTopLeft.x = [self childFolderWindowLeftForWidth:windowWidth];
+ NSPoint top = NSMakePoint(0, (NSMaxY([parentButton_ frame]) +
+ bookmarks::kBookmarkVerticalPadding));
+ NSPoint topOfWindow =
+ [[parentButton_ window]
+ convertBaseToScreen:[[parentButton_ superview]
+ convertPoint:top toView:nil]];
+ newWindowTopLeft.y = topOfWindow.y;
+ }
+ return newWindowTopLeft;
+}
+
+// Set our window level to the right spot so we're above the menubar, dock, etc.
+// Factored out so we can override/noop in a unit test.
+- (void)configureWindowLevel {
+ [[self window] setLevel:NSPopUpMenuWindowLevel];
+}
+
+- (int)windowHeightForButtonCount:(int)buttonCount {
+ return (buttonCount * bookmarks::kBookmarkButtonVerticalSpan) +
+ bookmarks::kBookmarkVerticalPadding;
+}
+
+- (void)adjustWindowForHeight:(int)windowHeight {
+ // Adjust all button widths to be consistent, determine the best size for
+ // the window, and set the window frame.
+ CGFloat windowWidth =
+ [self adjustButtonWidths] +
+ (2 * bookmarks::kBookmarkSubMenuHorizontalPadding);
+ NSPoint newWindowTopLeft = [self windowTopLeftForWidth:windowWidth];
+ NSSize windowSize = NSMakeSize(windowWidth, windowHeight);
+ windowSize = [scrollView_ convertSize:windowSize toView:nil];
+ NSWindow* window = [self window];
+ // If the window is already visible then make sure its top remains stable.
+ BOOL windowAlreadyShowing = [window isVisible];
+ CGFloat deltaY = windowHeight - NSHeight([mainView_ frame]);
+ if (windowAlreadyShowing) {
+ NSRect oldFrame = [window frame];
+ newWindowTopLeft.y = oldFrame.origin.y + NSHeight(oldFrame);
+ }
+ NSRect windowFrame = NSMakeRect(newWindowTopLeft.x,
+ newWindowTopLeft.y - windowHeight, windowSize.width, windowHeight);
+ // Make the scrolled content be the right size (full size).
+ NSRect mainViewFrame = NSMakeRect(0, 0, NSWidth(windowFrame) -
+ bookmarks::kScrollViewContentWidthMargin, NSHeight(windowFrame));
+ [mainView_ setFrame:mainViewFrame];
+ // Make sure the window fits on the screen. If not, constrain.
+ // We'll scroll to allow the user to see all the content.
+ NSRect screenFrame = [[[self window] screen] frame];
+ screenFrame = NSInsetRect(screenFrame, 0, kScrollWindowVerticalMargin);
+ BOOL wasScrollable = scrollable_;
+ if (!NSContainsRect(screenFrame, windowFrame)) {
+ scrollable_ = YES;
+ windowFrame = NSIntersectionRect(screenFrame, windowFrame);
+ } else {
+ scrollable_ = NO;
+ }
+ [window setFrame:windowFrame display:NO];
+ if (wasScrollable != scrollable_) {
+ // If scrollability changed then rework visibility of the scroll arrows
+ // and the scroll offset of the menu view.
+ NSSize windowLocalSize =
+ [scrollView_ convertSize:windowFrame.size fromView:nil];
+ CGFloat scrollPointY = NSHeight(mainViewFrame) - windowLocalSize.height +
+ bookmarks::kBookmarkVerticalPadding;
+ [mainView_ scrollPoint:NSMakePoint(0, scrollPointY)];
+ [self showOrHideScrollArrows];
+ [self addOrUpdateScrollTracking];
+ } else if (scrollable_ && windowAlreadyShowing) {
+ // If the window was already showing and is still scrollable then make
+ // sure the main view moves upward, not downward so that the content
+ // at the bottom of the menu, not the top, appears to move.
+ // The edge case is when the menu is scrolled all the way to top (hence
+ // the test of scrollDownArrowShown_) - don't scroll then.
+ NSView* superView = [mainView_ superview];
+ DCHECK([superView isKindOfClass:[NSClipView class]]);
+ NSClipView* clipView = static_cast<NSClipView*>(superView);
+ CGFloat scrollPointY = [clipView bounds].origin.y +
+ bookmarks::kBookmarkVerticalPadding;
+ if (scrollDownArrowShown_ || deltaY > 0.0)
+ scrollPointY += deltaY;
+ [mainView_ scrollPoint:NSMakePoint(0, scrollPointY)];
+ }
+ [window display];
+}
+
+// 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 {
+ const BookmarkNode* node = [parentButton_ bookmarkNode];
+ DCHECK(node);
+ int startingIndex = [[parentButton_ cell] startingChildIndex];
+ DCHECK_LE(startingIndex, node->GetChildCount());
+ // Must have at least 1 button (for "empty")
+ int buttons = std::max(node->GetChildCount() - startingIndex, 1);
+
+ // Prelim height of the window. We'll trim later as needed.
+ int height = [self windowHeightForButtonCount:buttons];
+ // We'll need this soon...
+ [self window];
+
+ // TODO(jrg): combine with frame code in bookmark_bar_controller.mm
+ // http://crbug.com/35966
+ NSRect buttonsOuterFrame = NSMakeRect(
+ bookmarks::kBookmarkSubMenuHorizontalPadding,
+ (height - bookmarks::kBookmarkButtonVerticalSpan),
+ bookmarks::kDefaultBookmarkWidth,
+ bookmarks::kBookmarkButtonHeight);
+
+ // 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:buttonsOuterFrame];
+ [buttons_ addObject:button];
+ [mainView_ addSubview:button];
+ } else {
+ for (int i = startingIndex;
+ i < node->GetChildCount();
+ i++) {
+ const BookmarkNode* child = node->GetChild(i);
+ BookmarkButton* button = [self makeButtonForNode:child
+ frame:buttonsOuterFrame];
+ [buttons_ addObject:button];
+ [mainView_ addSubview:button];
+ buttonsOuterFrame.origin.y -= bookmarks::kBookmarkButtonVerticalSpan;
+ }
+ }
+
+ [self adjustWindowForHeight:height];
+ // Finally pop me up.
+ [self configureWindowLevel];
+}
+
+// TODO(mrossetti): See if the following can be moved into view's viewWillDraw:.
+- (CGFloat)adjustButtonWidths {
+ CGFloat width = bookmarks::kBookmarkMenuButtonMinimumWidth;
+ // Use the cell's size as the base for determining the desired width of the
+ // button rather than the button's current width. -[cell cellSize] always
+ // returns the 'optimum' size of the cell based on the cell's contents even
+ // if it's less than the current button size. Relying on the button size
+ // would result in buttons that could only get wider but we want to handle
+ // the case where the widest button gets removed from a folder menu.
+ for (BookmarkButton* button in buttons_.get())
+ width = std::max(width, [[button cell] cellSize].width);
+ width = std::min(width, bookmarks::kBookmarkMenuButtonMaximumWidth);
+ // Things look and feel more menu-like if all the buttons are the
+ // full width of the window, especially if there are submenus.
+ for (BookmarkButton* button in buttons_.get()) {
+ NSRect buttonFrame = [button frame];
+ buttonFrame.size.width = width;
+ [button setFrame:buttonFrame];
+ }
+ return width;
+}
+
+- (BOOL)canScrollUp {
+ // If removal of an arrow would make things "finished", state as
+ // such.
+ CGFloat scrollY = [scrollView_ documentVisibleRect].origin.y;
+ if (scrollUpArrowShown_)
+ scrollY -= verticalScrollArrowHeight_;
+
+ if (scrollY <= 0)
+ return NO;
+ return YES;
+}
+
+- (BOOL)canScrollDown {
+ CGFloat arrowAdjustment = 0.0;
+
+ // We do NOT adjust based on the scrollDOWN arrow. This keeps
+ // things from "jumping"; if removal of the down arrow (at the top
+ // of the window) would cause a scroll to end, we'll end.
+ if (scrollUpArrowShown_)
+ arrowAdjustment += verticalScrollArrowHeight_;
+
+ NSPoint scrollPosition = [scrollView_ documentVisibleRect].origin;
+ NSRect documentRect = [[scrollView_ documentView] frame];
+
+ // If we are exactly the right height, return no. We need this
+ // extra conditional in the case where we've just scrolled/grown
+ // into position.
+ if (NSHeight([[self window] frame]) == NSHeight(documentRect))
+ return NO;
+
+ if ((scrollPosition.y + NSHeight([[self window] frame])) >=
+ (NSHeight(documentRect) + arrowAdjustment)) {
+ return NO;
+ }
+ return YES;
+}
+
+- (void)showOrHideScrollArrows {
+ NSRect frame = [scrollView_ frame];
+ CGFloat scrollDelta = 0.0;
+ BOOL canScrollDown = [self canScrollDown];
+ BOOL canScrollUp = [self canScrollUp];
+
+ if (canScrollUp != scrollUpArrowShown_) {
+ if (scrollUpArrowShown_) {
+ frame.origin.y -= verticalScrollArrowHeight_;
+ frame.size.height += verticalScrollArrowHeight_;
+ scrollDelta = verticalScrollArrowHeight_;
+ } else {
+ frame.origin.y += verticalScrollArrowHeight_;
+ frame.size.height -= verticalScrollArrowHeight_;
+ scrollDelta = -verticalScrollArrowHeight_;
+ }
+ }
+ if (canScrollDown != scrollDownArrowShown_) {
+ if (scrollDownArrowShown_) {
+ frame.size.height += verticalScrollArrowHeight_;
+ } else {
+ frame.size.height -= verticalScrollArrowHeight_;
+ }
+ }
+ scrollUpArrowShown_ = canScrollUp;
+ scrollDownArrowShown_ = canScrollDown;
+ [scrollView_ setFrame:frame];
+
+ // Adjust scroll based on new frame. For example, if we make room
+ // for an arrow at the bottom, adjust the scroll so the topmost item
+ // is still fully visible.
+ if (scrollDelta) {
+ NSPoint scrollPosition = [scrollView_ documentVisibleRect].origin;
+ scrollPosition.y -= scrollDelta;
+ [[scrollView_ documentView] scrollPoint:scrollPosition];
+ }
+}
+
+- (BOOL)scrollable {
+ return scrollable_;
+}
+
+// Start a "scroll up" timer.
+- (void)beginScrollWindowUp {
+ [self addScrollTimerWithDelta:kBookmarkBarFolderScrollAmount];
+}
+
+// Start a "scroll down" timer.
+- (void)beginScrollWindowDown {
+ [self addScrollTimerWithDelta:-kBookmarkBarFolderScrollAmount];
+}
+
+// End a scrolling timer. Can be called excessively with no harm.
+- (void)endScroll {
+ if (scrollTimer_) {
+ [scrollTimer_ invalidate];
+ scrollTimer_ = nil;
+ verticalScrollDelta_ = 0;
+ }
+}
+
+// Perform a single scroll of the specified amount.
+// Scroll up:
+// Scroll the documentView by the growth amount.
+// If we cannot grow the window, simply scroll the documentView.
+// If we can grow the window up without falling off the screen, do it.
+// Scroll down:
+// Never change the window size; only scroll the documentView.
+- (void)performOneScroll:(CGFloat)delta {
+ NSRect windowFrame = [[self window] frame];
+ NSRect screenFrame = [[[self window] screen] frame];
+
+ // First scroll the "document" area.
+ NSPoint scrollPosition = [scrollView_ documentVisibleRect].origin;
+ scrollPosition.y -= delta;
+ [[scrollView_ documentView] scrollPoint:scrollPosition];
+
+ if (buttonThatMouseIsIn_)
+ [buttonThatMouseIsIn_ toggleButtonBorderingWhileMouseInside];
+
+ // We update the window size after shifting the scroll to avoid a race.
+ CGFloat screenHeightMinusMargin = (NSHeight(screenFrame) -
+ (2 * kScrollWindowVerticalMargin));
+ if (delta) {
+ // If we can, grow the window (up).
+ if (NSHeight(windowFrame) < screenHeightMinusMargin) {
+ CGFloat growAmount = delta;
+ // Don't scroll more than enough to "finish".
+ if (scrollPosition.y < 0)
+ growAmount += scrollPosition.y;
+ windowFrame.size.height += growAmount;
+ windowFrame.size.height = std::min(NSHeight(windowFrame),
+ screenHeightMinusMargin);
+ // Watch out for a finish that isn't the full height of the screen.
+ // We get here if using the scroll wheel to scroll by small amounts.
+ windowFrame.size.height = std::min(NSHeight(windowFrame),
+ NSHeight([mainView_ frame]));
+ // Don't allow scrolling to make the window smaller, ever. This
+ // conditional is important when processing scrollWheel events.
+ if (windowFrame.size.height > [[self window] frame].size.height) {
+ [[self window] setFrame:windowFrame display:YES];
+ [self addOrUpdateScrollTracking];
+ }
+ }
+ }
+
+ // If we're at either end, happiness.
+ if ((scrollPosition.y <= 0) ||
+ ((scrollPosition.y + NSHeight(windowFrame) >=
+ NSHeight([mainView_ frame])) &&
+ (windowFrame.size.height == screenHeightMinusMargin))) {
+ [self endScroll];
+
+ // If we can't scroll either up or down we are completely done.
+ // For example, perhaps we've scrolled a little and grown the
+ // window on-screen until there is now room for everything.
+ if (![self canScrollUp] && ![self canScrollDown]) {
+ scrollable_ = NO;
+ [self removeScrollTracking];
+ }
+ }
+
+ [self showOrHideScrollArrows];
+}
+
+// Perform a scroll of the window on the screen.
+// Called by a timer when scrolling.
+- (void)performScroll:(NSTimer*)timer {
+ DCHECK(verticalScrollDelta_);
+ [self performOneScroll:verticalScrollDelta_];
+}
+
+
+// Add a timer to fire at a regular interveral which scrolls the
+// window vertically |delta|.
+- (void)addScrollTimerWithDelta:(CGFloat)delta {
+ if (scrollTimer_ && verticalScrollDelta_ == delta)
+ return;
+ [self endScroll];
+ verticalScrollDelta_ = delta;
+ scrollTimer_ =
+ [NSTimer scheduledTimerWithTimeInterval:kBookmarkBarFolderScrollInterval
+ target:self
+ selector:@selector(performScroll:)
+ userInfo:nil
+ repeats:YES];
+}
+
+// Called as a result of our tracking area. Warning: on the main
+// screen (of a single-screened machine), the minimum mouse y value is
+// 1, not 0. Also, we do not get events when the mouse is above the
+// menubar (to be fixed by setting the proper window level; see
+// initializer).
+- (void)mouseMoved:(NSEvent*)theEvent {
+ DCHECK([theEvent window] == [self window]);
+
+ NSPoint eventScreenLocation =
+ [[theEvent window] convertBaseToScreen:[theEvent locationInWindow]];
+
+ // We use frame (not visibleFrame) since our bookmark folder is on
+ // TOP of the menubar.
+ NSRect visibleRect = [[[self window] screen] frame];
+ CGFloat closeToTopOfScreen = NSMaxY(visibleRect) -
+ verticalScrollArrowHeight_;
+ CGFloat closeToBottomOfScreen = NSMinY(visibleRect) +
+ verticalScrollArrowHeight_;
+
+ if (eventScreenLocation.y <= closeToBottomOfScreen) {
+ [self beginScrollWindowUp];
+ } else if (eventScreenLocation.y > closeToTopOfScreen) {
+ [self beginScrollWindowDown];
+ } else {
+ [self endScroll];
+ }
+}
+
+- (void)mouseExited:(NSEvent*)theEvent {
+ [self endScroll];
+}
+
+// Add a tracking area so we know when the mouse is pinned to the top
+// or bottom of the screen. If that happens, and if the mouse
+// position overlaps the window, scroll it.
+- (void)addOrUpdateScrollTracking {
+ [self removeScrollTracking];
+ NSView* view = [[self window] contentView];
+ scrollTrackingArea_.reset([[NSTrackingArea alloc]
+ initWithRect:[view bounds]
+ options:(NSTrackingMouseMoved |
+ NSTrackingMouseEnteredAndExited |
+ NSTrackingActiveAlways)
+ owner:self
+ userInfo:nil]);
+ [view addTrackingArea:scrollTrackingArea_];
+}
+
+// Remove the tracking area associated with scrolling.
+- (void)removeScrollTracking {
+ if (scrollTrackingArea_.get()) {
+ [[[self window] contentView] removeTrackingArea:scrollTrackingArea_];
+ }
+ scrollTrackingArea_.reset();
+}
+
+// Delegate callback.
+- (void)windowWillClose:(NSNotification*)notification {
+ // If a "hover open" is pending when the bookmark bar folder is
+ // closed, be sure it gets cancelled.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+
+ [barController_ childFolderWillClose:self];
+ [self closeBookmarkFolder:self];
+ [self autorelease];
+}
+
+// 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])
+ [folderTarget_ openBookmarkFolderFromButton:sender];
+}
+
+- (NSArray*)buttons {
+ return buttons_.get();
+}
+
+- (void)close {
+ [folderController_ close];
+ [super close];
+}
+
+- (void)scrollWheel:(NSEvent *)theEvent {
+ if (scrollable_) {
+ // We go negative since an NSScrollView has a flipped coordinate frame.
+ CGFloat amt = kBookmarkBarFolderScrollWheelAmount * -[theEvent deltaY];
+ [self performOneScroll:amt];
+ }
+}
+
+#pragma mark Actions Forwarded to Parent BookmarkBarController
+
+- (IBAction)openBookmark:(id)sender {
+ [barController_ openBookmark:sender];
+}
+
+- (IBAction)openBookmarkInNewForegroundTab:(id)sender {
+ [barController_ openBookmarkInNewForegroundTab:sender];
+}
+
+- (IBAction)openBookmarkInNewWindow:(id)sender {
+ [barController_ openBookmarkInNewWindow:sender];
+}
+
+- (IBAction)openBookmarkInIncognitoWindow:(id)sender {
+ [barController_ openBookmarkInIncognitoWindow:sender];
+}
+
+- (IBAction)editBookmark:(id)sender {
+ [barController_ editBookmark:sender];
+}
+
+- (IBAction)cutBookmark:(id)sender {
+ [self closeBookmarkFolder:self];
+ [barController_ cutBookmark:sender];
+}
+
+- (IBAction)copyBookmark:(id)sender {
+ [barController_ copyBookmark:sender];
+}
+
+- (IBAction)pasteBookmark:(id)sender {
+ [barController_ pasteBookmark:sender];
+}
+
+- (IBAction)deleteBookmark:(id)sender {
+ [self closeBookmarkFolder:self];
+ [barController_ deleteBookmark:sender];
+}
+
+- (IBAction)openAllBookmarks:(id)sender {
+ [barController_ openAllBookmarks:sender];
+}
+
+- (IBAction)openAllBookmarksNewWindow:(id)sender {
+ [barController_ openAllBookmarksNewWindow:sender];
+}
+
+- (IBAction)openAllBookmarksIncognitoWindow:(id)sender {
+ [barController_ openAllBookmarksIncognitoWindow:sender];
+}
+
+- (IBAction)addPage:(id)sender {
+ [barController_ addPage:sender];
+}
+
+- (IBAction)addFolder:(id)sender {
+ [barController_ addFolder:sender];
+}
+
+#pragma mark Drag & Drop
+
+// 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
+- (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;
+}
+
+// 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)indexForDragToPoint:(NSPoint)point {
+ // 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);
+ // Be careful if the number of buttons != number of nodes.
+ return ((beforeNode->GetParent()->IndexOfChild(beforeNode) + 1) -
+ [[parentButton_ cell] startingChildIndex]);
+}
+
+// TODO(jrg): Yet more code dup.
+// http://crbug.com/35966
+- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode
+ to:(NSPoint)point
+ copy:(BOOL)copy {
+ DCHECK(sourceNode);
+
+ // Drop destination.
+ const BookmarkNode* destParent = NULL;
+ int destIndex = 0;
+
+ // First check if we're dropping on a button. If we have one, and
+ // it's a folder, drop in it.
+ BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
+ if ([button isFolder]) {
+ destParent = [button bookmarkNode];
+ // Drop it at the end.
+ destIndex = [button bookmarkNode]->GetChildCount();
+ } else {
+ // Else we're dropping somewhere in the folder, so find the right spot.
+ destParent = [parentButton_ bookmarkNode];
+ destIndex = [self indexForDragToPoint:point];
+ // Be careful if the number of buttons != number of nodes.
+ destIndex += [[parentButton_ cell] startingChildIndex];
+ }
+
+ // Prevent cycles.
+ BOOL wasCopiedOrMoved = NO;
+ if (!destParent->HasAncestor(sourceNode)) {
+ if (copy)
+ [self bookmarkModel]->Copy(sourceNode, destParent, destIndex);
+ else
+ [self bookmarkModel]->Move(sourceNode, destParent, destIndex);
+ wasCopiedOrMoved = YES;
+ // Movement of a node triggers observers (like us) to rebuild the
+ // bar so we don't have to do so explicitly.
+ }
+
+ return wasCopiedOrMoved;
+}
+
+#pragma mark BookmarkButtonDelegate Protocol
+
+- (void)fillPasteboard:(NSPasteboard*)pboard
+ forDragOfButton:(BookmarkButton*)button {
+ [[self folderTarget] fillPasteboard:pboard forDragOfButton:button];
+
+ // Close our folder menu and submenus since we know we're going to be dragged.
+ [self closeBookmarkFolder:self];
+}
+
+// Called from BookmarkButton.
+// Unlike bookmark_bar_controller's version, we DO default to being enabled.
+- (void)mouseEnteredButton:(id)sender event:(NSEvent*)event {
+ buttonThatMouseIsIn_ = sender;
+
+ // 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 {
+ if (buttonThatMouseIsIn_ == sender)
+ buttonThatMouseIsIn_ = nil;
+
+ // 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];
+}
+
+- (NSWindow*)browserWindow {
+ return [parentController_ browserWindow];
+}
+
+- (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button {
+ return [barController_ canEditBookmark:[button bookmarkNode]];
+}
+
+- (void)didDragBookmarkToTrash:(BookmarkButton*)button {
+ // TODO(mrossetti): Refactor BookmarkBarFolder common code.
+ // http://crbug.com/35966
+ const BookmarkNode* node = [button bookmarkNode];
+ if (node) {
+ const BookmarkNode* parent = node->GetParent();
+ [self bookmarkModel]->Remove(parent,
+ parent->IndexOfChild(node));
+ }
+}
+
+#pragma mark BookmarkButtonControllerProtocol
+
+// Recursively close all bookmark folders.
+- (void)closeAllBookmarkFolders {
+ // Closing the top level implicitly closes all children.
+ [barController_ closeAllBookmarkFolders];
+}
+
+// Close our bookmark folder (a sub-controller) if we have one.
+- (void)closeBookmarkFolder:(id)sender {
+ if (folderController_) {
+ [self setSubFolderGrowthToRight:YES];
+ [[folderController_ window] close];
+ folderController_ = nil;
+ }
+}
+
+- (BookmarkModel*)bookmarkModel {
+ return [barController_ bookmarkModel];
+}
+
+// TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966
+// 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 {
+ NSPoint currentLocation = [info draggingLocation];
+ BookmarkButton* button = [self buttonForDroppingOnAtPoint:currentLocation];
+
+ // Don't allow drops that would result in cycles.
+ if (button) {
+ NSData* data = [[info draggingPasteboard]
+ dataForType:kBookmarkButtonDragType];
+ if (data && [info draggingSource]) {
+ BookmarkButton* sourceButton = nil;
+ [data getBytes:&sourceButton length:sizeof(sourceButton)];
+ const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
+ const BookmarkNode* destNode = [button bookmarkNode];
+ if (destNode->HasAncestor(sourceNode))
+ button = nil;
+ }
+ }
+ // Delegate handling of dragging over a button to the |hoverState_| member.
+ return [hoverState_ draggingEnteredButton:button];
+}
+
+// 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 {
+ // NOT the same as a cancel --> we may have moved the mouse into the submenu.
+ // Delegate handling of the hover button to the |hoverState_| member.
+ [hoverState_ draggingExited];
+}
+
+- (BOOL)dragShouldLockBarVisibility {
+ return [parentController_ dragShouldLockBarVisibility];
+}
+
+// TODO(jrg): ARGH more code dup.
+// http://crbug.com/35966
+- (BOOL)dragButton:(BookmarkButton*)sourceButton
+ to:(NSPoint)point
+ copy:(BOOL)copy {
+ DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]);
+ const BookmarkNode* sourceNode = [sourceButton bookmarkNode];
+ return [self dragBookmark:sourceNode to:point copy:copy];
+}
+
+// TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController.
+// http://crbug.com/35966
+- (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info {
+ BOOL dragged = NO;
+ std::vector<const BookmarkNode*> nodes([self retrieveBookmarkNodeData]);
+ if (nodes.size()) {
+ BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove);
+ NSPoint dropPoint = [info draggingLocation];
+ for (std::vector<const BookmarkNode*>::const_iterator it = nodes.begin();
+ it != nodes.end(); ++it) {
+ const BookmarkNode* sourceNode = *it;
+ dragged = [self dragBookmark:sourceNode to:dropPoint copy:copy];
+ }
+ }
+ return dragged;
+}
+
+// TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController.
+// http://crbug.com/35966
+- (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData {
+ std::vector<const BookmarkNode*> dragDataNodes;
+ BookmarkNodeData dragData;
+ if(dragData.ReadFromDragClipboard()) {
+ BookmarkModel* bookmarkModel = [self bookmarkModel];
+ Profile* profile = bookmarkModel->profile();
+ std::vector<const BookmarkNode*> nodes(dragData.GetNodes(profile));
+ dragDataNodes.assign(nodes.begin(), nodes.end());
+ }
+ return dragDataNodes;
+}
+
+// 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)indicatorPosForDragToPoint:(NSPoint)point {
+ CGFloat y = 0;
+ int destIndex = [self indexForDragToPoint: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 = NSMaxY(buttonFrame) + 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;
+}
+
+- (ThemeProvider*)themeProvider {
+ return [parentController_ themeProvider];
+}
+
+- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child {
+ // Do nothing.
+}
+
+- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child {
+ // Do nothing.
+}
+
+- (BookmarkBarFolderController*)folderController {
+ return folderController_;
+}
+
+// Add a new folder controller as triggered by the given folder button.
+- (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton {
+ if (folderController_)
+ [self closeBookmarkFolder:self];
+
+ // Folder controller, like many window controllers, owns itself.
+ folderController_ =
+ [[BookmarkBarFolderController alloc] initWithParentButton:parentButton
+ parentController:self
+ barController:barController_];
+ [folderController_ showWindow:self];
+}
+
+- (void)openAll:(const BookmarkNode*)node
+ disposition:(WindowOpenDisposition)disposition {
+ [barController_ openAll:node disposition:disposition];
+}
+
+- (void)addButtonForNode:(const BookmarkNode*)node
+ atIndex:(NSInteger)buttonIndex {
+ // Propose the frame for the new button. By default, this will be set to the
+ // topmost button's frame (and there will always be one) offset upward in
+ // anticipation of insertion.
+ NSRect newButtonFrame = [[buttons_ objectAtIndex:0] frame];
+ newButtonFrame.origin.y += bookmarks::kBookmarkButtonVerticalSpan;
+ // When adding a button to an empty folder we must remove the 'empty'
+ // placeholder button. This can be detected by checking for a parent
+ // child count of 1.
+ const BookmarkNode* parentNode = node->GetParent();
+ if (parentNode->GetChildCount() == 1) {
+ BookmarkButton* emptyButton = [buttons_ lastObject];
+ newButtonFrame = [emptyButton frame];
+ [emptyButton setDelegate:nil];
+ [emptyButton removeFromSuperview];
+ [buttons_ removeLastObject];
+ }
+
+ if (buttonIndex == -1 || buttonIndex > (NSInteger)[buttons_ count])
+ buttonIndex = [buttons_ count];
+
+ // Offset upward by one button height all buttons above insertion location.
+ BookmarkButton* button = nil; // Remember so it can be de-highlighted.
+ for (NSInteger i = 0; i < buttonIndex; ++i) {
+ button = [buttons_ objectAtIndex:i];
+ // Remember this location in case it's the last button being moved
+ // which is where the new button will be located.
+ newButtonFrame = [button frame];
+ NSRect buttonFrame = [button frame];
+ buttonFrame.origin.y += bookmarks::kBookmarkButtonVerticalSpan;
+ [button setFrame:buttonFrame];
+ }
+ [[button cell] mouseExited:nil]; // De-highlight.
+ BookmarkButton* newButton = [self makeButtonForNode:node
+ frame:newButtonFrame];
+ [buttons_ insertObject:newButton atIndex:buttonIndex];
+ [mainView_ addSubview:newButton];
+
+ // Close any child folder(s) which may still be open.
+ [self closeBookmarkFolder:self];
+
+ // Prelim height of the window. We'll trim later as needed.
+ int height = [self windowHeightForButtonCount:[buttons_ count]];
+ [self adjustWindowForHeight:height];
+}
+
+// More code which essentially duplicates that of BookmarkBarController.
+// TODO(mrossetti,jrg): http://crbug.com/35966
+- (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point {
+ DCHECK([urls count] == [titles count]);
+ BOOL nodesWereAdded = NO;
+ // Figure out where these new bookmarks nodes are to be added.
+ BookmarkButton* button = [self buttonForDroppingOnAtPoint:point];
+ BookmarkModel* bookmarkModel = [self bookmarkModel];
+ const BookmarkNode* destParent = NULL;
+ int destIndex = 0;
+ if ([button isFolder]) {
+ destParent = [button bookmarkNode];
+ // Drop it at the end.
+ destIndex = [button bookmarkNode]->GetChildCount();
+ } else {
+ // Else we're dropping somewhere in the folder, so find the right spot.
+ destParent = [parentButton_ bookmarkNode];
+ destIndex = [self indexForDragToPoint:point];
+ // Be careful if the number of buttons != number of nodes.
+ destIndex += [[parentButton_ cell] startingChildIndex];
+ }
+
+ // Create and add the new bookmark nodes.
+ size_t urlCount = [urls count];
+ for (size_t i = 0; i < urlCount; ++i) {
+ GURL gurl;
+ const char* string = [[urls objectAtIndex:i] UTF8String];
+ if (string)
+ gurl = GURL(string);
+ // We only expect to receive valid URLs.
+ DCHECK(gurl.is_valid());
+ if (gurl.is_valid()) {
+ bookmarkModel->AddURL(destParent,
+ destIndex++,
+ base::SysNSStringToUTF16([titles objectAtIndex:i]),
+ gurl);
+ nodesWereAdded = YES;
+ }
+ }
+ return nodesWereAdded;
+}
+
+- (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex {
+ if (fromIndex != toIndex) {
+ if (toIndex == -1)
+ toIndex = [buttons_ count];
+ BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex];
+ [buttons_ removeObjectAtIndex:fromIndex];
+ NSRect movedFrame = [movedButton frame];
+ NSPoint toOrigin = movedFrame.origin;
+ [movedButton setHidden:YES];
+ if (fromIndex < toIndex) {
+ BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex - 1];
+ toOrigin = [targetButton frame].origin;
+ for (NSInteger i = fromIndex; i < toIndex; ++i) {
+ BookmarkButton* button = [buttons_ objectAtIndex:i];
+ NSRect frame = [button frame];
+ frame.origin.y += bookmarks::kBookmarkButtonVerticalSpan;
+ [button setFrameOrigin:frame.origin];
+ }
+ } else {
+ BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex];
+ toOrigin = [targetButton frame].origin;
+ for (NSInteger i = fromIndex - 1; i >= toIndex; --i) {
+ BookmarkButton* button = [buttons_ objectAtIndex:i];
+ NSRect buttonFrame = [button frame];
+ buttonFrame.origin.y -= bookmarks::kBookmarkButtonVerticalSpan;
+ [button setFrameOrigin:buttonFrame.origin];
+ }
+ }
+ [buttons_ insertObject:movedButton atIndex:toIndex];
+ [movedButton setFrameOrigin:toOrigin];
+ [movedButton setHidden:NO];
+ }
+}
+
+// TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966
+- (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate {
+ // TODO(mrossetti): Get disappearing animation to work. http://crbug.com/42360
+ BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex];
+ NSPoint poofPoint = [oldButton screenLocationForRemoveAnimation];
+
+ // If a hover-open is pending, cancel it.
+ if (oldButton == buttonThatMouseIsIn_) {
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+ buttonThatMouseIsIn_ = nil;
+ }
+
+ // Deleting a button causes rearrangement that enables us to lose a
+ // mouse-exited event. This problem doesn't appear to exist with
+ // other keep-menu-open options (e.g. add folder). Since the
+ // showsBorderOnlyWhileMouseInside uses a tracking area, simple
+ // tricks (e.g. sending an extra mouseExited: to the button) don't
+ // fix the problem.
+ // http://crbug.com/54324
+ for (NSButton* button in buttons_.get()) {
+ if ([button showsBorderOnlyWhileMouseInside]) {
+ [button setShowsBorderOnlyWhileMouseInside:NO];
+ [button setShowsBorderOnlyWhileMouseInside:YES];
+ }
+ }
+
+ [oldButton setDelegate:nil];
+ [oldButton removeFromSuperview];
+ if (animate && !ignoreAnimations_)
+ NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint,
+ NSZeroSize, nil, nil, nil);
+ [buttons_ removeObjectAtIndex:buttonIndex];
+ for (NSInteger i = 0; i < buttonIndex; ++i) {
+ BookmarkButton* button = [buttons_ objectAtIndex:i];
+ NSRect buttonFrame = [button frame];
+ buttonFrame.origin.y -= bookmarks::kBookmarkButtonVerticalSpan;
+ [button setFrame:buttonFrame];
+ }
+ // Search for and adjust submenus, if necessary.
+ NSInteger buttonCount = [buttons_ count];
+ if (buttonCount) {
+ BookmarkButton* subButton = [folderController_ parentButton];
+ for (NSInteger i = buttonIndex; i < buttonCount; ++i) {
+ BookmarkButton* aButton = [buttons_ objectAtIndex:i];
+ // If this button is showing its menu then we need to move the menu, too.
+ if (aButton == subButton)
+ [folderController_ offsetFolderMenuWindow:NSMakeSize(0.0,
+ bookmarks::kBookmarkBarHeight)];
+ }
+ } else {
+ // If all nodes have been removed from this folder then add in the
+ // 'empty' placeholder button.
+ NSRect buttonFrame =
+ NSMakeRect(bookmarks::kBookmarkSubMenuHorizontalPadding,
+ bookmarks::kBookmarkButtonHeight -
+ (bookmarks::kBookmarkBarHeight -
+ bookmarks::kBookmarkVerticalPadding),
+ bookmarks::kDefaultBookmarkWidth,
+ (bookmarks::kBookmarkBarHeight -
+ 2 * bookmarks::kBookmarkVerticalPadding));
+ BookmarkButton* button = [self makeButtonForNode:nil
+ frame:buttonFrame];
+ [buttons_ addObject:button];
+ [mainView_ addSubview:button];
+ buttonCount = 1;
+ }
+
+ // Propose a height for the window. We'll trim later as needed.
+ [self adjustWindowForHeight:[self windowHeightForButtonCount:buttonCount]];
+}
+
+- (id<BookmarkButtonControllerProtocol>)controllerForNode:
+ (const BookmarkNode*)node {
+ // See if we are holding this node, otherwise see if it is in our
+ // hierarchy of visible folder menus.
+ if ([parentButton_ bookmarkNode] == node)
+ return self;
+ return [folderController_ controllerForNode:node];
+}
+
+#pragma mark TestingAPI Only
+
+- (void)setIgnoreAnimations:(BOOL)ignore {
+ ignoreAnimations_ = ignore;
+}
+
+- (BookmarkButton*)buttonThatMouseIsIn {
+ return buttonThatMouseIsIn_;
+}
+
+@end // BookmarkBarFolderController
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller_unittest.mm
new file mode 100644
index 0000000..cae8766
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller_unittest.mm
@@ -0,0 +1,1552 @@
+// 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"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/view_resizer_pong.h"
+#include "chrome/test/model_test_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+// Add a redirect to make testing easier.
+@interface BookmarkBarFolderController(MakeTestingEasier)
+- (IBAction)openBookmarkFolderFromButton:(id)sender;
+- (void)validateMenuSpacing;
+@end
+
+@implementation BookmarkBarFolderController(MakeTestingEasier)
+- (IBAction)openBookmarkFolderFromButton:(id)sender {
+ [[self folderTarget] openBookmarkFolderFromButton:sender];
+}
+
+// Utility function to verify that the buttons in this folder are all
+// evenly spaced in a progressive manner.
+- (void)validateMenuSpacing {
+ BOOL firstButton = YES;
+ CGFloat lastVerticalOffset = 0.0;
+ for (BookmarkButton* button in [self buttons]) {
+ if (firstButton) {
+ firstButton = NO;
+ lastVerticalOffset = [button frame].origin.y;
+ } else {
+ CGFloat nextVerticalOffset = [button frame].origin.y;
+ EXPECT_CGFLOAT_EQ(lastVerticalOffset -
+ bookmarks::kBookmarkButtonVerticalSpan,
+ nextVerticalOffset);
+ lastVerticalOffset = nextVerticalOffset;
+ }
+ }
+}
+@end
+
+// Don't use a high window level when running unit tests -- it'll
+// interfere with anything else you are working on.
+// For testing.
+@interface BookmarkBarFolderControllerNoLevel : BookmarkBarFolderController
+@end
+
+@implementation BookmarkBarFolderControllerNoLevel
+- (void)configureWindowLevel {
+ // Intentionally empty.
+}
+@end
+
+// No window level and the ability to fake the "top left" point of the window.
+// For testing.
+@interface BookmarkBarFolderControllerLow : BookmarkBarFolderControllerNoLevel {
+ BOOL realTopLeft_; // Use the real windowTopLeft call?
+}
+@property (nonatomic) BOOL realTopLeft;
+@end
+
+
+@implementation BookmarkBarFolderControllerLow
+
+@synthesize realTopLeft = realTopLeft_;
+
+- (NSPoint)windowTopLeftForWidth:(int)width {
+ return realTopLeft_ ? [super windowTopLeftForWidth:width] :
+ NSMakePoint(200,200);
+}
+
+@end
+
+
+@interface BookmarkBarFolderControllerPong : BookmarkBarFolderControllerLow {
+ BOOL childFolderWillShow_;
+ BOOL childFolderWillClose_;
+}
+@property (nonatomic, readonly) BOOL childFolderWillShow;
+@property (nonatomic, readonly) BOOL childFolderWillClose;
+@end
+
+@implementation BookmarkBarFolderControllerPong
+@synthesize childFolderWillShow = childFolderWillShow_;
+@synthesize childFolderWillClose = childFolderWillClose_;
+
+- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child {
+ childFolderWillShow_ = YES;
+}
+
+- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child {
+ childFolderWillClose_ = YES;
+}
+
+// We don't have a real BookmarkBarController as our parent root so
+// we fake this one out.
+- (void)closeAllBookmarkFolders {
+ [self closeBookmarkFolder:self];
+}
+
+@end
+
+namespace {
+const int kLotsOfNodesCount = 150;
+};
+
+
+// Redirect certain calls so they can be seen by tests.
+
+@interface BookmarkBarControllerChildFolderRedirect : BookmarkBarController {
+ BookmarkBarFolderController* childFolderDelegate_;
+}
+@property (nonatomic, assign) BookmarkBarFolderController* childFolderDelegate;
+@end
+
+@implementation BookmarkBarControllerChildFolderRedirect
+
+@synthesize childFolderDelegate = childFolderDelegate_;
+
+- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child {
+ [childFolderDelegate_ childFolderWillShow:child];
+}
+
+- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child {
+ [childFolderDelegate_ childFolderWillClose:child];
+}
+
+@end
+
+
+class BookmarkBarFolderControllerTest : public CocoaTest {
+ public:
+ BrowserTestHelper helper_;
+ scoped_nsobject<BookmarkBarControllerChildFolderRedirect> bar_;
+ const BookmarkNode* folderA_; // owned by model
+ const BookmarkNode* longTitleNode_; // owned by model
+
+ BookmarkBarFolderControllerTest() {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ const BookmarkNode* folderA = model->AddGroup(parent,
+ parent->GetChildCount(),
+ ASCIIToUTF16("group"));
+ folderA_ = folderA;
+ model->AddGroup(parent, parent->GetChildCount(),
+ ASCIIToUTF16("sibbling group"));
+ const BookmarkNode* folderB = model->AddGroup(folderA,
+ folderA->GetChildCount(),
+ ASCIIToUTF16("subgroup 1"));
+ model->AddGroup(folderA,
+ folderA->GetChildCount(),
+ ASCIIToUTF16("subgroup 2"));
+ model->AddURL(folderA, folderA->GetChildCount(), ASCIIToUTF16("title a"),
+ GURL("http://www.google.com/a"));
+ longTitleNode_ = model->AddURL(
+ folderA, folderA->GetChildCount(),
+ ASCIIToUTF16("title super duper long long whoa momma title you betcha"),
+ GURL("http://www.google.com/b"));
+ model->AddURL(folderB, folderB->GetChildCount(), ASCIIToUTF16("t"),
+ GURL("http://www.google.com/c"));
+
+ bar_.reset(
+ [[BookmarkBarControllerChildFolderRedirect alloc]
+ initWithBrowser:helper_.browser()
+ initialWidth:300
+ delegate:nil
+ resizeDelegate:nil]);
+ [bar_ loaded:model];
+ // Make parent frame for bookmark bar then open it.
+ NSRect frame = [[test_window() contentView] frame];
+ frame = NSInsetRect(frame, 100, 200);
+ NSView* fakeToolbarView = [[[NSView alloc] initWithFrame:frame]
+ autorelease];
+ [[test_window() contentView] addSubview:fakeToolbarView];
+ [fakeToolbarView addSubview:[bar_ view]];
+ [bar_ setBookmarkBarEnabled:YES];
+ }
+
+ // Remove the bookmark with the long title.
+ void RemoveLongTitleNode() {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ model->Remove(longTitleNode_->GetParent(),
+ longTitleNode_->GetParent()->IndexOfChild(longTitleNode_));
+ }
+
+ // Add LOTS of nodes to our model if needed (e.g. scrolling).
+ // Returns the number of nodes added.
+ int AddLotsOfNodes() {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ for (int i = 0; i < kLotsOfNodesCount; i++) {
+ model->AddURL(folderA_, folderA_->GetChildCount(),
+ ASCIIToUTF16("repeated title"),
+ GURL("http://www.google.com/repeated/url"));
+ }
+ return kLotsOfNodesCount;
+ }
+
+ // Return a simple BookmarkBarFolderController.
+ BookmarkBarFolderControllerPong* SimpleBookmarkBarFolderController() {
+ BookmarkButton* parentButton = [[bar_ buttons] objectAtIndex:0];
+ BookmarkBarFolderControllerPong* c =
+ [[BookmarkBarFolderControllerPong alloc]
+ initWithParentButton:parentButton
+ parentController:nil
+ barController:bar_];
+ [c window]; // Force nib load.
+ return c;
+ }
+};
+
+TEST_F(BookmarkBarFolderControllerTest, InitCreateAndDelete) {
+ scoped_nsobject<BookmarkBarFolderController> bbfc;
+ bbfc.reset(SimpleBookmarkBarFolderController());
+
+ // Make sure none of the buttons overlap, that all are inside
+ // the content frame, and their cells are of the proper class.
+ 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]));
+ }
+ Class cellClass = [BookmarkBarFolderButtonCell class];
+ for (BookmarkButton* button in buttons) {
+ NSRect r = [[bbfc mainView] convertRect:[button frame] fromView:button];
+ // TODO(jrg): remove this adjustment.
+ NSRect bigger = NSInsetRect([[bbfc mainView] frame], -2, 0);
+ EXPECT_TRUE(NSContainsRect(bigger, r));
+ EXPECT_TRUE([[button cell] isKindOfClass:cellClass]);
+ }
+
+ // 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, BasicPosition) {
+ BookmarkButton* parentButton = [[bar_ buttons] objectAtIndex:0];
+ EXPECT_TRUE(parentButton);
+
+ // If parent is a BookmarkBarController, grow down.
+ scoped_nsobject<BookmarkBarFolderControllerLow> bbfc;
+ bbfc.reset([[BookmarkBarFolderControllerLow alloc]
+ initWithParentButton:parentButton
+ parentController:nil
+ barController:bar_]);
+ [bbfc window];
+ [bbfc setRealTopLeft:YES];
+ NSPoint pt = [bbfc windowTopLeftForWidth:0]; // screen coords
+ NSPoint buttonOriginInScreen =
+ [[parentButton window]
+ convertBaseToScreen:[parentButton
+ convertRectToBase:[parentButton frame]].origin];
+ // Within margin
+ EXPECT_LE(abs(pt.x - buttonOriginInScreen.x),
+ bookmarks::kBookmarkMenuOverlap+1);
+ EXPECT_LE(abs(pt.y - buttonOriginInScreen.y),
+ bookmarks::kBookmarkMenuOverlap+1);
+
+ // Make sure we see the window shift left if it spills off the screen
+ pt = [bbfc windowTopLeftForWidth:0];
+ NSPoint shifted = [bbfc windowTopLeftForWidth:9999999];
+ EXPECT_LT(shifted.x, pt.x);
+
+ // If parent is a BookmarkBarFolderController, grow right.
+ scoped_nsobject<BookmarkBarFolderControllerLow> bbfc2;
+ bbfc2.reset([[BookmarkBarFolderControllerLow alloc]
+ initWithParentButton:[[bbfc buttons] objectAtIndex:0]
+ parentController:bbfc.get()
+ barController:bar_]);
+ [bbfc2 window];
+ [bbfc2 setRealTopLeft:YES];
+ pt = [bbfc2 windowTopLeftForWidth:0];
+ // We're now overlapping the window a bit.
+ EXPECT_EQ(pt.x, NSMaxX([[bbfc.get() window] frame]) -
+ bookmarks::kBookmarkMenuOverlap);
+}
+
+// Confirm we grow right until end of screen, then start growing left
+// until end of screen again, then right.
+TEST_F(BookmarkBarFolderControllerTest, PositionRightLeftRight) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ const BookmarkNode* folder = parent;
+
+ const int count = 100;
+ int i;
+ // Make some super duper deeply nested folders.
+ for (i=0; i<count; i++) {
+ folder = model->AddGroup(folder, 0, ASCIIToUTF16("nested folder"));
+ }
+
+ // Setup initial state for opening all folders.
+ folder = parent;
+ BookmarkButton* parentButton = [[bar_ buttons] objectAtIndex:0];
+ BookmarkBarFolderController* parentController = nil;
+ EXPECT_TRUE(parentButton);
+
+ // Open them all.
+ scoped_nsobject<NSMutableArray> folder_controller_array;
+ folder_controller_array.reset([[NSMutableArray array] retain]);
+ for (i=0; i<count; i++) {
+ BookmarkBarFolderControllerNoLevel* bbfcl =
+ [[BookmarkBarFolderControllerNoLevel alloc]
+ initWithParentButton:parentButton
+ parentController:parentController
+ barController:bar_];
+ [folder_controller_array addObject:bbfcl];
+ [bbfcl autorelease];
+ [bbfcl window];
+ parentController = bbfcl;
+ parentButton = [[bbfcl buttons] objectAtIndex:0];
+ }
+
+ // Make vector of all x positions.
+ std::vector<CGFloat> leftPositions;
+ for (i=0; i<count; i++) {
+ CGFloat x = [[[folder_controller_array objectAtIndex:i] window]
+ frame].origin.x;
+ leftPositions.push_back(x);
+ }
+
+ // Make sure the first few grow right.
+ for (i=0; i<3; i++)
+ EXPECT_TRUE(leftPositions[i+1] > leftPositions[i]);
+
+ // Look for the first "grow left".
+ while (leftPositions[i] > leftPositions[i-1])
+ i++;
+ // Confirm the next few also grow left.
+ int j;
+ for (j=i; j<i+3; j++)
+ EXPECT_TRUE(leftPositions[j+1] < leftPositions[j]);
+ i = j;
+
+ // Finally, confirm we see a "grow right" once more.
+ while (leftPositions[i] < leftPositions[i-1])
+ i++;
+ // (No need to EXPECT a final "grow right"; if we didn't find one
+ // we'd get a C++ array bounds exception).
+}
+
+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. We don't loop over all
+ // buttons because the scroll view makes them not visible.
+ 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)]);;
+ }
+ }
+}
+
+TEST_F(BookmarkBarFolderControllerTest, OpenFolder) {
+ scoped_nsobject<BookmarkBarFolderController> bbfc;
+ bbfc.reset(SimpleBookmarkBarFolderController());
+ EXPECT_TRUE(bbfc.get());
+
+ EXPECT_FALSE([bbfc folderController]);
+ BookmarkButton* button = [[bbfc buttons] objectAtIndex:0];
+ [bbfc openBookmarkFolderFromButton:button];
+ id controller = [bbfc folderController];
+ EXPECT_TRUE(controller);
+ EXPECT_EQ([controller parentButton], button);
+
+ // Click the same one --> it gets closed.
+ [bbfc openBookmarkFolderFromButton:[[bbfc buttons] objectAtIndex:0]];
+ EXPECT_FALSE([bbfc folderController]);
+
+ // Open a new one --> change.
+ [bbfc openBookmarkFolderFromButton:[[bbfc buttons] objectAtIndex:1]];
+ EXPECT_NE(controller, [bbfc folderController]);
+ EXPECT_NE([[bbfc folderController] parentButton], button);
+
+ // Close it --> all gone!
+ [bbfc closeBookmarkFolder:nil];
+ EXPECT_FALSE([bbfc folderController]);
+}
+
+TEST_F(BookmarkBarFolderControllerTest, ChildFolderCallbacks) {
+ scoped_nsobject<BookmarkBarFolderControllerPong> bbfc;
+ bbfc.reset(SimpleBookmarkBarFolderController());
+ EXPECT_TRUE(bbfc.get());
+ [bar_ setChildFolderDelegate:bbfc.get()];
+
+ EXPECT_FALSE([bbfc childFolderWillShow]);
+ [bbfc openBookmarkFolderFromButton:[[bbfc buttons] objectAtIndex:0]];
+ EXPECT_TRUE([bbfc childFolderWillShow]);
+
+ EXPECT_FALSE([bbfc childFolderWillClose]);
+ [bbfc closeBookmarkFolder:nil];
+ EXPECT_TRUE([bbfc childFolderWillClose]);
+
+ [bar_ setChildFolderDelegate:nil];
+}
+
+// Make sure bookmark folders have variable widths.
+TEST_F(BookmarkBarFolderControllerTest, ChildFolderWidth) {
+ scoped_nsobject<BookmarkBarFolderController> bbfc;
+
+ bbfc.reset(SimpleBookmarkBarFolderController());
+ EXPECT_TRUE(bbfc.get());
+ [bbfc showWindow:bbfc.get()];
+ CGFloat wideWidth = NSWidth([[bbfc window] frame]);
+
+ RemoveLongTitleNode();
+ bbfc.reset(SimpleBookmarkBarFolderController());
+ EXPECT_TRUE(bbfc.get());
+ CGFloat thinWidth = NSWidth([[bbfc window] frame]);
+
+ // Make sure window size changed as expected.
+ EXPECT_GT(wideWidth, thinWidth);
+}
+
+// Simple scrolling tests.
+TEST_F(BookmarkBarFolderControllerTest, SimpleScroll) {
+ scoped_nsobject<BookmarkBarFolderController> bbfc;
+
+ int nodecount = AddLotsOfNodes();
+ bbfc.reset(SimpleBookmarkBarFolderController());
+ EXPECT_TRUE(bbfc.get());
+ [bbfc showWindow:bbfc.get()];
+
+ // Make sure the window fits on the screen.
+ EXPECT_LT(NSHeight([[bbfc window] frame]),
+ NSHeight([[NSScreen mainScreen] frame]));
+
+ // Verify the logic used by the scroll arrow code.
+ EXPECT_TRUE([bbfc canScrollUp]);
+ EXPECT_FALSE([bbfc canScrollDown]);
+
+ // Scroll it up. Make sure the window has gotten bigger each time.
+ // Also, for each scroll, make sure our hit test finds a new button
+ // (to confirm the content area changed).
+ NSView* savedHit = nil;
+ for (int i=0; i<3; i++) {
+ CGFloat height = NSHeight([[bbfc window] frame]);
+ [bbfc performOneScroll:60];
+ EXPECT_GT(NSHeight([[bbfc window] frame]), height);
+ NSView* hit = [[[bbfc window] contentView] hitTest:NSMakePoint(22, 22)];
+ EXPECT_NE(hit, savedHit);
+ savedHit = hit;
+ }
+
+ // Keep scrolling up; make sure we never get bigger than the screen.
+ // Also confirm we never scroll the window off the screen.
+ bool bothAtOnce = false;
+ NSRect screenFrame = [[NSScreen mainScreen] frame];
+ for (int i = 0; i < nodecount; i++) {
+ [bbfc performOneScroll:60];
+ EXPECT_TRUE(NSContainsRect(screenFrame,
+ [[bbfc window] frame]));
+ // Make sure, sometime during our scroll, we have the ability to
+ // scroll in either direction.
+ if ([bbfc canScrollUp] &&
+ [bbfc canScrollDown])
+ bothAtOnce = true;
+ }
+ EXPECT_TRUE(bothAtOnce);
+
+ // Once we've scrolled to the end, our only option should be to scroll back.
+ EXPECT_FALSE([bbfc canScrollUp]);
+ EXPECT_TRUE([bbfc canScrollDown]);
+
+ // Now scroll down and make sure the window size does not change.
+ // Also confirm we never scroll the window off the screen the other
+ // way.
+ for (int i=0; i<nodecount+50; i++) {
+ CGFloat height = NSHeight([[bbfc window] frame]);
+ [bbfc performOneScroll:-60];
+ EXPECT_EQ(height, NSHeight([[bbfc window] frame]));
+ EXPECT_TRUE(NSContainsRect(screenFrame,
+ [[bbfc window] frame]));
+ }
+}
+
+// Folder menu sizing and placementwhile deleting bookmarks and scrolling tests.
+TEST_F(BookmarkBarFolderControllerTest, MenuPlacementWhileScrollingDeleting) {
+ scoped_nsobject<BookmarkBarFolderController> bbfc;
+ AddLotsOfNodes();
+ bbfc.reset(SimpleBookmarkBarFolderController());
+ [bbfc showWindow:bbfc.get()];
+ NSWindow* menuWindow = [bbfc window];
+ BookmarkBarFolderController* folder = [bar_ folderController];
+ NSArray* buttons = [folder buttons];
+
+ // Before scrolling any, delete a bookmark and make sure the window top has
+ // not moved. Pick a button which is near the top and visible.
+ CGFloat oldTop = [menuWindow frame].origin.y + NSHeight([menuWindow frame]);
+ BookmarkButton* button = [buttons objectAtIndex:3];
+ [folder deleteBookmark:button];
+ CGFloat newTop = [menuWindow frame].origin.y + NSHeight([menuWindow frame]);
+ EXPECT_CGFLOAT_EQ(oldTop, newTop);
+
+ // Scroll so that both the top and bottom scroll arrows show, make sure
+ // the top of the window has moved up, then delete a visible button and
+ // make sure the top has not moved.
+ oldTop = newTop;
+ const CGFloat scrollOneBookmark = bookmarks::kBookmarkButtonHeight +
+ bookmarks::kBookmarkVerticalPadding;
+ NSUInteger buttonCounter = 0;
+ NSUInteger extraButtonLimit = 3;
+ while (![bbfc canScrollDown] || extraButtonLimit > 0) {
+ [bbfc performOneScroll:scrollOneBookmark];
+ ++buttonCounter;
+ if ([bbfc canScrollDown])
+ --extraButtonLimit;
+ }
+ newTop = [menuWindow frame].origin.y + NSHeight([menuWindow frame]);
+ EXPECT_NE(oldTop, newTop);
+ oldTop = newTop;
+ button = [buttons objectAtIndex:buttonCounter + 3];
+ [folder deleteBookmark:button];
+ newTop = [menuWindow frame].origin.y + NSHeight([menuWindow frame]);
+ EXPECT_CGFLOAT_EQ(oldTop, newTop);
+
+ // Scroll so that the top scroll arrow is no longer showing, make sure
+ // the top of the window has not moved, then delete a visible button and
+ // make sure the top has not moved.
+ while ([bbfc canScrollDown]) {
+ [bbfc performOneScroll:-scrollOneBookmark];
+ --buttonCounter;
+ }
+ button = [buttons objectAtIndex:buttonCounter + 3];
+ [folder deleteBookmark:button];
+ newTop = [menuWindow frame].origin.y + NSHeight([menuWindow frame]);
+ EXPECT_CGFLOAT_EQ(oldTop, newTop);
+}
+
+@interface FakedDragInfo : NSObject {
+@public
+ NSPoint dropLocation_;
+ NSDragOperation sourceMask_;
+}
+@property (nonatomic, assign) NSPoint dropLocation;
+- (void)setDraggingSourceOperationMask:(NSDragOperation)mask;
+@end
+
+@implementation FakedDragInfo
+
+@synthesize dropLocation = dropLocation_;
+
+- (id)init {
+ if ((self = [super init])) {
+ dropLocation_ = NSZeroPoint;
+ sourceMask_ = NSDragOperationMove;
+ }
+ return self;
+}
+
+// NSDraggingInfo protocol functions.
+
+- (id)draggingPasteboard {
+ return self;
+}
+
+- (id)draggingSource {
+ return self;
+}
+
+- (NSDragOperation)draggingSourceOperationMask {
+ return sourceMask_;
+}
+
+- (NSPoint)draggingLocation {
+ return dropLocation_;
+}
+
+// Other functions.
+
+- (void)setDraggingSourceOperationMask:(NSDragOperation)mask {
+ sourceMask_ = mask;
+}
+
+@end
+
+
+class BookmarkBarFolderControllerMenuTest : public CocoaTest {
+ public:
+ BrowserTestHelper helper_;
+ scoped_nsobject<NSView> parent_view_;
+ scoped_nsobject<ViewResizerPong> resizeDelegate_;
+ scoped_nsobject<BookmarkBarController> bar_;
+
+ BookmarkBarFolderControllerMenuTest() {
+ resizeDelegate_.reset([[ViewResizerPong alloc] init]);
+ NSRect parent_frame = NSMakeRect(0, 0, 800, 50);
+ parent_view_.reset([[NSView alloc] initWithFrame:parent_frame]);
+ [parent_view_ setHidden:YES];
+ bar_.reset([[BookmarkBarController alloc]
+ initWithBrowser:helper_.browser()
+ initialWidth:NSWidth(parent_frame)
+ delegate:nil
+ resizeDelegate:resizeDelegate_.get()]);
+ InstallAndToggleBar(bar_.get());
+ }
+
+ void InstallAndToggleBar(BookmarkBarController* bar) {
+ // Force loading of the nib.
+ [bar view];
+ // Awkwardness to look like we've been installed.
+ [parent_view_ addSubview:[bar view]];
+ NSRect frame = [[[bar view] superview] frame];
+ 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
+ withAnimation:NO];
+ }
+};
+
+TEST_F(BookmarkBarFolderControllerMenuTest, DragMoveBarBookmarkToFolder) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b "
+ "2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ 4f2f1b "
+ "4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actualModelString = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModelString);
+
+ // Pop up a folder menu and drag in a button from the bar.
+ BookmarkButton* toFolder = [bar_ buttonWithTitleEqualTo:@"2f"];
+ NSRect oldToFolderFrame = [toFolder frame];
+ [[toFolder target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:toFolder];
+ BookmarkBarFolderController* folderController = [bar_ folderController];
+ EXPECT_TRUE(folderController);
+ NSWindow* toWindow = [folderController window];
+ EXPECT_TRUE(toWindow);
+ NSRect oldToWindowFrame = [toWindow frame];
+ // Drag a bar button onto a bookmark (i.e. not a folder) in a folder
+ // so it should end up below the target bookmark.
+ BookmarkButton* draggedButton = [bar_ buttonWithTitleEqualTo:@"1b"];
+ ASSERT_TRUE(draggedButton);
+ CGFloat horizontalShift =
+ NSWidth([draggedButton frame]) + bookmarks::kBookmarkHorizontalPadding;
+ BookmarkButton* targetButton =
+ [folderController buttonWithTitleEqualTo:@"2f1b"];
+ ASSERT_TRUE(targetButton);
+ [folderController dragButton:draggedButton
+ to:[targetButton center]
+ copy:NO];
+ // The button should have landed just after "2f1b".
+ const std::string expected_string("2f:[ 2f1b 1b 2f2f:[ 2f2f1b "
+ "2f2f2b 2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ "
+ "4f2f1b 4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b ");
+ EXPECT_EQ(expected_string, model_test_utils::ModelStringFromNode(root));
+
+ // Verify the window still appears by looking for its controller.
+ EXPECT_TRUE([bar_ folderController]);
+
+ // Gather the new frames.
+ NSRect newToFolderFrame = [toFolder frame];
+ NSRect newToWindowFrame = [toWindow frame];
+ // The toFolder should have shifted left horizontally but not vertically.
+ NSRect expectedToFolderFrame =
+ NSOffsetRect(oldToFolderFrame, -horizontalShift, 0);
+ EXPECT_NSRECT_EQ(expectedToFolderFrame, newToFolderFrame);
+ // The toWindow should have shifted left horizontally, down vertically,
+ // and grown vertically.
+ NSRect expectedToWindowFrame = oldToWindowFrame;
+ expectedToWindowFrame.origin.x -= horizontalShift;
+ CGFloat diff = (bookmarks::kBookmarkBarHeight +
+ 2*bookmarks::kBookmarkVerticalPadding);
+ expectedToWindowFrame.origin.y -= diff;
+ expectedToWindowFrame.size.height += diff;
+ EXPECT_NSRECT_EQ(expectedToWindowFrame, newToWindowFrame);
+
+ // Check button spacing.
+ [folderController validateMenuSpacing];
+
+ // Move the button back to the bar at the beginning.
+ draggedButton = [folderController buttonWithTitleEqualTo:@"1b"];
+ ASSERT_TRUE(draggedButton);
+ targetButton = [bar_ buttonWithTitleEqualTo:@"2f"];
+ ASSERT_TRUE(targetButton);
+ [bar_ dragButton:draggedButton
+ to:[targetButton left]
+ copy:NO];
+ EXPECT_EQ(model_string, model_test_utils::ModelStringFromNode(root));
+ // Don't check the folder window since it's not supposed to be showing.
+}
+
+TEST_F(BookmarkBarFolderControllerMenuTest, DragCopyBarBookmarkToFolder) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b "
+ "2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ 4f2f1b "
+ "4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actualModelString = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModelString);
+
+ // Pop up a folder menu and copy in a button from the bar.
+ BookmarkButton* toFolder = [bar_ buttonWithTitleEqualTo:@"2f"];
+ ASSERT_TRUE(toFolder);
+ NSRect oldToFolderFrame = [toFolder frame];
+ [[toFolder target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:toFolder];
+ BookmarkBarFolderController* folderController = [bar_ folderController];
+ EXPECT_TRUE(folderController);
+ NSWindow* toWindow = [folderController window];
+ EXPECT_TRUE(toWindow);
+ NSRect oldToWindowFrame = [toWindow frame];
+ // Drag a bar button onto a bookmark (i.e. not a folder) in a folder
+ // so it should end up below the target bookmark.
+ BookmarkButton* draggedButton = [bar_ buttonWithTitleEqualTo:@"1b"];
+ ASSERT_TRUE(draggedButton);
+ BookmarkButton* targetButton =
+ [folderController buttonWithTitleEqualTo:@"2f1b"];
+ ASSERT_TRUE(targetButton);
+ [folderController dragButton:draggedButton
+ to:[targetButton center]
+ copy:YES];
+ // The button should have landed just after "2f1b".
+ const std::string expected_1("1b 2f:[ 2f1b 1b 2f2f:[ 2f2f1b "
+ "2f2f2b 2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ "
+ "4f2f1b 4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b ");
+ EXPECT_EQ(expected_1, model_test_utils::ModelStringFromNode(root));
+
+ // Gather the new frames.
+ NSRect newToFolderFrame = [toFolder frame];
+ NSRect newToWindowFrame = [toWindow frame];
+ // The toFolder should have shifted.
+ EXPECT_NSRECT_EQ(oldToFolderFrame, newToFolderFrame);
+ // The toWindow should have shifted down vertically and grown vertically.
+ NSRect expectedToWindowFrame = oldToWindowFrame;
+ CGFloat diff = (bookmarks::kBookmarkBarHeight +
+ 2*bookmarks::kBookmarkVerticalPadding);
+ expectedToWindowFrame.origin.y -= diff;
+ expectedToWindowFrame.size.height += diff;
+ EXPECT_NSRECT_EQ(expectedToWindowFrame, newToWindowFrame);
+
+ // Copy the button back to the bar after "3b".
+ draggedButton = [folderController buttonWithTitleEqualTo:@"1b"];
+ ASSERT_TRUE(draggedButton);
+ targetButton = [bar_ buttonWithTitleEqualTo:@"4f"];
+ ASSERT_TRUE(targetButton);
+ [bar_ dragButton:draggedButton
+ to:[targetButton left]
+ copy:YES];
+ const std::string expected_2("1b 2f:[ 2f1b 1b 2f2f:[ 2f2f1b "
+ "2f2f2b 2f2f3b ] 2f3b ] 3b 1b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ "
+ "4f2f1b 4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b ");
+ EXPECT_EQ(expected_2, model_test_utils::ModelStringFromNode(root));
+}
+
+TEST_F(BookmarkBarFolderControllerMenuTest, DragMoveBarBookmarkToSubfolder) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b "
+ "2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ 4f2f1b "
+ "4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actualModelString = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModelString);
+
+ // Pop up a folder menu and a subfolder menu.
+ BookmarkButton* toFolder = [bar_ buttonWithTitleEqualTo:@"4f"];
+ ASSERT_TRUE(toFolder);
+ [[toFolder target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:toFolder];
+ BookmarkBarFolderController* folderController = [bar_ folderController];
+ EXPECT_TRUE(folderController);
+ NSWindow* toWindow = [folderController window];
+ EXPECT_TRUE(toWindow);
+ NSRect oldToWindowFrame = [toWindow frame];
+ BookmarkButton* toSubfolder =
+ [folderController buttonWithTitleEqualTo:@"4f2f"];
+ ASSERT_TRUE(toSubfolder);
+ [[toSubfolder target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:toSubfolder];
+ BookmarkBarFolderController* subfolderController =
+ [folderController folderController];
+ EXPECT_TRUE(subfolderController);
+ NSWindow* toSubwindow = [subfolderController window];
+ EXPECT_TRUE(toSubwindow);
+ NSRect oldToSubwindowFrame = [toSubwindow frame];
+ // Drag a bar button onto a bookmark (i.e. not a folder) in a folder
+ // so it should end up below the target bookmark.
+ BookmarkButton* draggedButton = [bar_ buttonWithTitleEqualTo:@"5b"];
+ ASSERT_TRUE(draggedButton);
+ BookmarkButton* targetButton =
+ [subfolderController buttonWithTitleEqualTo:@"4f2f3b"];
+ ASSERT_TRUE(targetButton);
+ [subfolderController dragButton:draggedButton
+ to:[targetButton center]
+ copy:NO];
+ // The button should have landed just after "2f".
+ const std::string expected_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b "
+ "2f2f2b 2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ "
+ "4f2f1b 4f2f2b 4f2f3b 5b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] ");
+ EXPECT_EQ(expected_string, model_test_utils::ModelStringFromNode(root));
+
+ // Check button spacing.
+ [folderController validateMenuSpacing];
+ [subfolderController validateMenuSpacing];
+
+ // Check the window layouts. The folder window should not have changed,
+ // but the subfolder window should have shifted vertically and grown.
+ NSRect newToWindowFrame = [toWindow frame];
+ EXPECT_NSRECT_EQ(oldToWindowFrame, newToWindowFrame);
+ NSRect newToSubwindowFrame = [toSubwindow frame];
+ NSRect expectedToSubwindowFrame = oldToSubwindowFrame;
+ expectedToSubwindowFrame.origin.y -=
+ bookmarks::kBookmarkBarHeight + bookmarks::kVisualHeightOffset;
+ expectedToSubwindowFrame.size.height +=
+ bookmarks::kBookmarkBarHeight + bookmarks::kVisualHeightOffset;
+ EXPECT_NSRECT_EQ(expectedToSubwindowFrame, newToSubwindowFrame);
+}
+
+TEST_F(BookmarkBarFolderControllerMenuTest, DragMoveWithinFolder) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b "
+ "2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ 4f2f1b "
+ "4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actualModelString = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModelString);
+
+ // Pop up a folder menu.
+ BookmarkButton* toFolder = [bar_ buttonWithTitleEqualTo:@"4f"];
+ ASSERT_TRUE(toFolder);
+ [[toFolder target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:toFolder];
+ BookmarkBarFolderController* folderController = [bar_ folderController];
+ EXPECT_TRUE(folderController);
+ NSWindow* toWindow = [folderController window];
+ EXPECT_TRUE(toWindow);
+ NSRect oldToWindowFrame = [toWindow frame];
+ // Drag a folder button to the top within the same parent.
+ BookmarkButton* draggedButton =
+ [folderController buttonWithTitleEqualTo:@"4f2f"];
+ ASSERT_TRUE(draggedButton);
+ BookmarkButton* targetButton =
+ [folderController buttonWithTitleEqualTo:@"4f1f"];
+ ASSERT_TRUE(targetButton);
+ [folderController dragButton:draggedButton
+ to:[targetButton top]
+ copy:NO];
+ // The button should have landed above "4f1f".
+ const std::string expected_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b "
+ "2f2f2b 2f2f3b ] 2f3b ] 3b 4f:[ 4f2f:[ 4f2f1b 4f2f2b 4f2f3b ] "
+ "4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b ");
+ EXPECT_EQ(expected_string, model_test_utils::ModelStringFromNode(root));
+
+ // The window should not have gone away.
+ EXPECT_TRUE([bar_ folderController]);
+
+ // The folder window should not have changed.
+ NSRect newToWindowFrame = [toWindow frame];
+ EXPECT_NSRECT_EQ(oldToWindowFrame, newToWindowFrame);
+
+ // Check button spacing.
+ [folderController validateMenuSpacing];
+}
+
+TEST_F(BookmarkBarFolderControllerMenuTest, DragParentOntoChild) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b "
+ "2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ 4f2f1b "
+ "4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actualModelString = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModelString);
+
+ // Pop up a folder menu.
+ BookmarkButton* toFolder = [bar_ buttonWithTitleEqualTo:@"4f"];
+ ASSERT_TRUE(toFolder);
+ [[toFolder target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:toFolder];
+ BookmarkBarFolderController* folderController = [bar_ folderController];
+ EXPECT_TRUE(folderController);
+ NSWindow* toWindow = [folderController window];
+ EXPECT_TRUE(toWindow);
+ // Drag a folder button to one of its children.
+ BookmarkButton* draggedButton = [bar_ buttonWithTitleEqualTo:@"4f"];
+ ASSERT_TRUE(draggedButton);
+ BookmarkButton* targetButton =
+ [folderController buttonWithTitleEqualTo:@"4f3f"];
+ ASSERT_TRUE(targetButton);
+ [folderController dragButton:draggedButton
+ to:[targetButton top]
+ copy:NO];
+ // The model should not have changed.
+ EXPECT_EQ(model_string, model_test_utils::ModelStringFromNode(root));
+
+ // Check button spacing.
+ [folderController validateMenuSpacing];
+}
+
+TEST_F(BookmarkBarFolderControllerMenuTest, DragMoveChildToParent) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b "
+ "2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ 4f2f1b "
+ "4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actualModelString = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModelString);
+
+ // Pop up a folder menu and a subfolder menu.
+ BookmarkButton* toFolder = [bar_ buttonWithTitleEqualTo:@"4f"];
+ ASSERT_TRUE(toFolder);
+ [[toFolder target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:toFolder];
+ BookmarkBarFolderController* folderController = [bar_ folderController];
+ EXPECT_TRUE(folderController);
+ BookmarkButton* toSubfolder =
+ [folderController buttonWithTitleEqualTo:@"4f2f"];
+ ASSERT_TRUE(toSubfolder);
+ [[toSubfolder target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:toSubfolder];
+ BookmarkBarFolderController* subfolderController =
+ [folderController folderController];
+ EXPECT_TRUE(subfolderController);
+
+ // Drag a subfolder bookmark to the parent folder.
+ BookmarkButton* draggedButton =
+ [subfolderController buttonWithTitleEqualTo:@"4f2f3b"];
+ ASSERT_TRUE(draggedButton);
+ BookmarkButton* targetButton =
+ [folderController buttonWithTitleEqualTo:@"4f2f"];
+ ASSERT_TRUE(targetButton);
+ [folderController dragButton:draggedButton
+ to:[targetButton top]
+ copy:NO];
+ // The button should have landed above "4f2f".
+ const std::string expected_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b "
+ "2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f3b 4f2f:[ "
+ "4f2f1b 4f2f2b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b ");
+ EXPECT_EQ(expected_string, model_test_utils::ModelStringFromNode(root));
+
+ // Check button spacing.
+ [folderController validateMenuSpacing];
+ // The window should not have gone away.
+ EXPECT_TRUE([bar_ folderController]);
+ // The subfolder should have gone away.
+ EXPECT_FALSE([folderController folderController]);
+}
+
+TEST_F(BookmarkBarFolderControllerMenuTest, DragWindowResizing) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string
+ model_string("a b:[ b1 b2 b3 ] reallyReallyLongBookmarkName c ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actualModelString = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModelString);
+
+ // Pop up a folder menu.
+ BookmarkButton* toFolder = [bar_ buttonWithTitleEqualTo:@"b"];
+ ASSERT_TRUE(toFolder);
+ [[toFolder target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:toFolder];
+ BookmarkBarFolderController* folderController = [bar_ folderController];
+ EXPECT_TRUE(folderController);
+ NSWindow* toWindow = [folderController window];
+ EXPECT_TRUE(toWindow);
+ CGFloat oldWidth = NSWidth([toWindow frame]);
+ // Drag the bookmark with a long name to the folder.
+ BookmarkButton* draggedButton =
+ [bar_ buttonWithTitleEqualTo:@"reallyReallyLongBookmarkName"];
+ ASSERT_TRUE(draggedButton);
+ BookmarkButton* targetButton =
+ [folderController buttonWithTitleEqualTo:@"b1"];
+ ASSERT_TRUE(targetButton);
+ [folderController dragButton:draggedButton
+ to:[targetButton center]
+ copy:NO];
+ // Verify the model change.
+ const std::string
+ expected_string("a b:[ b1 reallyReallyLongBookmarkName b2 b3 ] c ");
+ EXPECT_EQ(expected_string, model_test_utils::ModelStringFromNode(root));
+ // Verify the window grew. Just test a reasonable width gain.
+ CGFloat newWidth = NSWidth([toWindow frame]);
+ EXPECT_LT(oldWidth + 30.0, newWidth);
+}
+
+TEST_F(BookmarkBarFolderControllerMenuTest, MoveRemoveAddButtons) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2b 2f3b ] 3b 4b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actualModelString = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModelString);
+
+ // Pop up a folder menu.
+ BookmarkButton* toFolder = [bar_ buttonWithTitleEqualTo:@"2f"];
+ ASSERT_TRUE(toFolder);
+ [[toFolder target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:toFolder];
+ BookmarkBarFolderController* folder = [bar_ folderController];
+ EXPECT_TRUE(folder);
+
+ // Remember how many buttons are showing.
+ NSArray* buttons = [folder buttons];
+ NSUInteger oldDisplayedButtons = [buttons count];
+
+ // Move a button around a bit.
+ [folder moveButtonFromIndex:0 toIndex:2];
+ EXPECT_NSEQ(@"2f2b", [[buttons objectAtIndex:0] title]);
+ EXPECT_NSEQ(@"2f3b", [[buttons objectAtIndex:1] title]);
+ EXPECT_NSEQ(@"2f1b", [[buttons objectAtIndex:2] title]);
+ EXPECT_EQ(oldDisplayedButtons, [buttons count]);
+ [folder moveButtonFromIndex:2 toIndex:0];
+ EXPECT_NSEQ(@"2f1b", [[buttons objectAtIndex:0] title]);
+ EXPECT_NSEQ(@"2f2b", [[buttons objectAtIndex:1] title]);
+ EXPECT_NSEQ(@"2f3b", [[buttons objectAtIndex:2] title]);
+ EXPECT_EQ(oldDisplayedButtons, [buttons count]);
+
+ // Add a couple of buttons.
+ const BookmarkNode* node = root->GetChild(2); // Purloin an existing node.
+ [folder addButtonForNode:node atIndex:0];
+ EXPECT_NSEQ(@"3b", [[buttons objectAtIndex:0] title]);
+ EXPECT_NSEQ(@"2f1b", [[buttons objectAtIndex:1] title]);
+ EXPECT_NSEQ(@"2f2b", [[buttons objectAtIndex:2] title]);
+ EXPECT_NSEQ(@"2f3b", [[buttons objectAtIndex:3] title]);
+ EXPECT_EQ(oldDisplayedButtons + 1, [buttons count]);
+ node = root->GetChild(3);
+ [folder addButtonForNode:node atIndex:-1];
+ EXPECT_NSEQ(@"3b", [[buttons objectAtIndex:0] title]);
+ EXPECT_NSEQ(@"2f1b", [[buttons objectAtIndex:1] title]);
+ EXPECT_NSEQ(@"2f2b", [[buttons objectAtIndex:2] title]);
+ EXPECT_NSEQ(@"2f3b", [[buttons objectAtIndex:3] title]);
+ EXPECT_NSEQ(@"4b", [[buttons objectAtIndex:4] title]);
+ EXPECT_EQ(oldDisplayedButtons + 2, [buttons count]);
+
+ // Remove a couple of buttons.
+ [folder removeButton:4 animate:NO];
+ [folder removeButton:1 animate:NO];
+ EXPECT_NSEQ(@"3b", [[buttons objectAtIndex:0] title]);
+ EXPECT_NSEQ(@"2f2b", [[buttons objectAtIndex:1] title]);
+ EXPECT_NSEQ(@"2f3b", [[buttons objectAtIndex:2] title]);
+ EXPECT_EQ(oldDisplayedButtons, [buttons count]);
+
+ // Check button spacing.
+ [folder validateMenuSpacing];
+}
+
+TEST_F(BookmarkBarFolderControllerMenuTest, ControllerForNode) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2b ] 3b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actualModelString = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModelString);
+
+ // Find the main bar controller.
+ const void* expectedController = bar_;
+ const void* actualController = [bar_ controllerForNode:root];
+ EXPECT_EQ(expectedController, actualController);
+
+ // Pop up the folder menu.
+ BookmarkButton* targetFolder = [bar_ buttonWithTitleEqualTo:@"2f"];
+ ASSERT_TRUE(targetFolder);
+ [[targetFolder target]
+ performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:targetFolder];
+ BookmarkBarFolderController* folder = [bar_ folderController];
+ EXPECT_TRUE(folder);
+
+ // Find the folder controller using the folder controller.
+ const BookmarkNode* targetNode = root->GetChild(1);
+ expectedController = folder;
+ actualController = [bar_ controllerForNode:targetNode];
+ EXPECT_EQ(expectedController, actualController);
+
+ // Find the folder controller from the bar.
+ actualController = [folder controllerForNode:targetNode];
+ EXPECT_EQ(expectedController, actualController);
+}
+
+TEST_F(BookmarkBarFolderControllerMenuTest, MenuSizingAndScrollArrows) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2b 3b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actualModelString = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModelString);
+
+ const BookmarkNode* parent = model.GetBookmarkBarNode();
+ const BookmarkNode* folder = model.AddGroup(parent,
+ parent->GetChildCount(),
+ ASCIIToUTF16("BIG"));
+
+ // Pop open the new folder window and verify it has one (empty) item.
+ BookmarkButton* button = [bar_ buttonWithTitleEqualTo:@"BIG"];
+ [[button target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:button];
+ BookmarkBarFolderController* folderController = [bar_ folderController];
+ EXPECT_TRUE(folderController);
+ NSWindow* folderMenu = [folderController window];
+ EXPECT_TRUE(folderMenu);
+ CGFloat expectedHeight = (CGFloat)bookmarks::kBookmarkButtonHeight +
+ (2*bookmarks::kBookmarkVerticalPadding);
+ NSRect menuFrame = [folderMenu frame];
+ CGFloat menuHeight = NSHeight(menuFrame);
+ EXPECT_CGFLOAT_EQ(expectedHeight, menuHeight);
+ EXPECT_FALSE([folderController scrollable]);
+
+ // Now add a real bookmark and reopen.
+ model.AddURL(folder, folder->GetChildCount(), ASCIIToUTF16("a"),
+ GURL("http://a.com/"));
+ folderController = [bar_ folderController];
+ EXPECT_TRUE(folderController);
+ folderMenu = [folderController window];
+ EXPECT_TRUE(folderMenu);
+ menuFrame = [folderMenu frame];
+ menuHeight = NSHeight(menuFrame);
+ EXPECT_CGFLOAT_EQ(expectedHeight, menuHeight);
+ CGFloat menuWidth = NSWidth(menuFrame);
+ button = [folderController buttonWithTitleEqualTo:@"a"];
+ CGFloat buttonWidth = NSWidth([button frame]);
+ CGFloat expectedWidth =
+ buttonWidth + (2 * bookmarks::kBookmarkSubMenuHorizontalPadding);
+ EXPECT_CGFLOAT_EQ(expectedWidth, menuWidth);
+
+ // Add a wider bookmark and make sure the button widths match.
+ model.AddURL(folder, folder->GetChildCount(),
+ ASCIIToUTF16("A really, really long name"),
+ GURL("http://www.google.com/a"));
+ EXPECT_LT(menuWidth, NSWidth([folderMenu frame]));
+ EXPECT_LT(buttonWidth, NSWidth([button frame]));
+ buttonWidth = NSWidth([button frame]);
+ BookmarkButton* buttonB =
+ [folderController buttonWithTitleEqualTo:@"A really, really long name"];
+ EXPECT_TRUE(buttonB);
+ CGFloat buttonWidthB = NSWidth([buttonB frame]);
+ EXPECT_CGFLOAT_EQ(buttonWidth, buttonWidthB);
+ // Add a bunch of bookmarks until the window grows no more, then check for
+ // a scroll down arrow.
+ CGFloat oldMenuHeight = 0.0; // It just has to be different for first run.
+ menuHeight = NSHeight([folderMenu frame]);
+ NSUInteger tripWire = 0; // Prevent a runaway.
+ while (![folderController scrollable] && ++tripWire < 100) {
+ model.AddURL(folder, folder->GetChildCount(), ASCIIToUTF16("B"),
+ GURL("http://b.com/"));
+ oldMenuHeight = menuHeight;
+ menuHeight = NSHeight([folderMenu frame]);
+ }
+ EXPECT_TRUE([folderController scrollable]);
+ EXPECT_TRUE([folderController canScrollUp]);
+
+ // Remove one bookmark and make sure the scroll down arrow has been removed.
+ // We'll remove the really long node so we can see if the buttons get resized.
+ menuWidth = NSWidth([folderMenu frame]);
+ buttonWidth = NSWidth([button frame]);
+ model.Remove(folder, 1);
+ EXPECT_FALSE([folderController scrollable]);
+ EXPECT_FALSE([folderController canScrollUp]);
+ EXPECT_FALSE([folderController canScrollDown]);
+
+ // Check the size. It should have reduced.
+ EXPECT_GT(menuWidth, NSWidth([folderMenu frame]));
+ EXPECT_GT(buttonWidth, NSWidth([button frame]));
+
+ // Check button spacing.
+ [folderController validateMenuSpacing];
+}
+
+// See http://crbug.com/46101
+TEST_F(BookmarkBarFolderControllerMenuTest, HoverThenDeleteBookmark) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const BookmarkNode* folder = model.AddGroup(root,
+ root->GetChildCount(),
+ ASCIIToUTF16("BIG"));
+ for (int i = 0; i < kLotsOfNodesCount; i++)
+ model.AddURL(folder, folder->GetChildCount(), ASCIIToUTF16("kid"),
+ GURL("http://kid.com/smile"));
+
+ // Pop open the new folder window and hover one of its kids.
+ BookmarkButton* button = [bar_ buttonWithTitleEqualTo:@"BIG"];
+ [[button target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:button];
+ BookmarkBarFolderController* bbfc = [bar_ folderController];
+ NSArray* buttons = [bbfc buttons];
+
+ // Hover over a button and verify that it is now known.
+ button = [buttons objectAtIndex:3];
+ BookmarkButton* buttonThatMouseIsIn = [bbfc buttonThatMouseIsIn];
+ EXPECT_FALSE(buttonThatMouseIsIn);
+ [bbfc mouseEnteredButton:button event:nil];
+ buttonThatMouseIsIn = [bbfc buttonThatMouseIsIn];
+ EXPECT_EQ(button, buttonThatMouseIsIn);
+
+ // Delete the bookmark and verify that it is now not known.
+ model.Remove(folder, 3);
+ buttonThatMouseIsIn = [bbfc buttonThatMouseIsIn];
+ EXPECT_FALSE(buttonThatMouseIsIn);
+}
+
+// Just like a BookmarkBarFolderController but intercedes when providing
+// pasteboard drag data.
+@interface BookmarkBarFolderControllerDragData : BookmarkBarFolderController {
+ const BookmarkNode* dragDataNode_; // Weak
+}
+- (void)setDragDataNode:(const BookmarkNode*)node;
+@end
+
+@implementation BookmarkBarFolderControllerDragData
+
+- (id)initWithParentButton:(BookmarkButton*)button
+ parentController:(BookmarkBarFolderController*)parentController
+ barController:(BookmarkBarController*)barController {
+ if ((self = [super initWithParentButton:button
+ parentController:parentController
+ barController:barController])) {
+ dragDataNode_ = NULL;
+ }
+ return self;
+}
+
+- (void)setDragDataNode:(const BookmarkNode*)node {
+ dragDataNode_ = node;
+}
+
+- (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData {
+ std::vector<const BookmarkNode*> dragDataNodes;
+ if(dragDataNode_) {
+ dragDataNodes.push_back(dragDataNode_);
+ }
+ return dragDataNodes;
+}
+
+@end
+
+TEST_F(BookmarkBarFolderControllerMenuTest, DragBookmarkData) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] "
+ "2f3b ] 3b 4b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+ const BookmarkNode* other = model.other_node();
+ const std::string other_string("O1b O2b O3f:[ O3f1b O3f2f ] "
+ "O4f:[ O4f1b O4f2f ] 05b ");
+ model_test_utils::AddNodesFromModelString(model, other, other_string);
+
+ // Validate initial model.
+ std::string actual = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actual);
+ actual = model_test_utils::ModelStringFromNode(other);
+ EXPECT_EQ(other_string, actual);
+
+ // Pop open a folder.
+ BookmarkButton* button = [bar_ buttonWithTitleEqualTo:@"2f"];
+ scoped_nsobject<BookmarkBarFolderControllerDragData> folderController;
+ folderController.reset([[BookmarkBarFolderControllerDragData alloc]
+ initWithParentButton:button
+ parentController:nil
+ barController:bar_]);
+ BookmarkButton* targetButton =
+ [folderController buttonWithTitleEqualTo:@"2f1b"];
+ ASSERT_TRUE(targetButton);
+
+ // Gen up some dragging data.
+ const BookmarkNode* newNode = other->GetChild(2);
+ [folderController setDragDataNode:newNode];
+ scoped_nsobject<FakedDragInfo> dragInfo([[FakedDragInfo alloc] init]);
+ [dragInfo setDropLocation:[targetButton top]];
+ [folderController dragBookmarkData:(id<NSDraggingInfo>)dragInfo.get()];
+
+ // Verify the model.
+ const std::string expected("1b 2f:[ O3f:[ O3f1b O3f2f ] 2f1b 2f2f:[ 2f2f1b "
+ "2f2f2b 2f2f3b ] 2f3b ] 3b 4b ");
+ actual = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(expected, actual);
+
+ // Now drag over a folder button.
+ targetButton = [folderController buttonWithTitleEqualTo:@"2f2f"];
+ ASSERT_TRUE(targetButton);
+ newNode = other->GetChild(2); // Should be O4f.
+ EXPECT_EQ(newNode->GetTitle(), ASCIIToUTF16("O4f"));
+ [folderController setDragDataNode:newNode];
+ [dragInfo setDropLocation:[targetButton center]];
+ [folderController dragBookmarkData:(id<NSDraggingInfo>)dragInfo.get()];
+
+ // Verify the model.
+ const std::string expectedA("1b 2f:[ O3f:[ O3f1b O3f2f ] 2f1b 2f2f:[ "
+ "2f2f1b 2f2f2b 2f2f3b O4f:[ O4f1b O4f2f ] ] "
+ "2f3b ] 3b 4b ");
+ actual = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(expectedA, actual);
+
+ // Check button spacing.
+ [folderController validateMenuSpacing];
+}
+
+TEST_F(BookmarkBarFolderControllerMenuTest, DragBookmarkDataToTrash) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] "
+ "2f3b ] 3b 4b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actual = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actual);
+
+ const BookmarkNode* folderNode = root->GetChild(1);
+ int oldFolderChildCount = folderNode->GetChildCount();
+
+ // Pop open a folder.
+ BookmarkButton* button = [bar_ buttonWithTitleEqualTo:@"2f"];
+ scoped_nsobject<BookmarkBarFolderControllerDragData> folderController;
+ folderController.reset([[BookmarkBarFolderControllerDragData alloc]
+ initWithParentButton:button
+ parentController:nil
+ barController:bar_]);
+
+ // Drag a button to the trash.
+ BookmarkButton* buttonToDelete =
+ [folderController buttonWithTitleEqualTo:@"2f1b"];
+ ASSERT_TRUE(buttonToDelete);
+ EXPECT_TRUE([folderController canDragBookmarkButtonToTrash:buttonToDelete]);
+ [folderController didDragBookmarkToTrash:buttonToDelete];
+
+ // There should be one less button in the folder.
+ int newFolderChildCount = folderNode->GetChildCount();
+ EXPECT_EQ(oldFolderChildCount - 1, newFolderChildCount);
+ // Verify the model.
+ const std::string expected("1b 2f:[ 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] "
+ "2f3b ] 3b 4b ");
+ actual = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(expected, actual);
+
+ // Check button spacing.
+ [folderController validateMenuSpacing];
+}
+
+TEST_F(BookmarkBarFolderControllerMenuTest, AddURLs) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] "
+ "2f3b ] 3b 4b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actual = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actual);
+
+ // Pop open a folder.
+ BookmarkButton* button = [bar_ buttonWithTitleEqualTo:@"2f"];
+ [[button target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:button];
+ BookmarkBarFolderController* folderController = [bar_ folderController];
+ EXPECT_TRUE(folderController);
+ NSArray* buttons = [folderController buttons];
+ EXPECT_TRUE(buttons);
+
+ // Remember how many buttons are showing.
+ int oldDisplayedButtons = [buttons count];
+
+ BookmarkButton* targetButton =
+ [folderController buttonWithTitleEqualTo:@"2f1b"];
+ ASSERT_TRUE(targetButton);
+
+ NSArray* urls = [NSArray arrayWithObjects: @"http://www.a.com/",
+ @"http://www.b.com/", nil];
+ NSArray* titles = [NSArray arrayWithObjects: @"SiteA", @"SiteB", nil];
+ [folderController addURLs:urls withTitles:titles at:[targetButton top]];
+
+ // There should two more buttons in the folder.
+ int newDisplayedButtons = [buttons count];
+ EXPECT_EQ(oldDisplayedButtons + 2, newDisplayedButtons);
+ // Verify the model.
+ const std::string expected("1b 2f:[ SiteA SiteB 2f1b 2f2f:[ 2f2f1b 2f2f2b "
+ "2f2f3b ] 2f3b ] 3b 4b ");
+ actual = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(expected, actual);
+
+ // Check button spacing.
+ [folderController validateMenuSpacing];
+}
+
+TEST_F(BookmarkBarFolderControllerMenuTest, DropPositionIndicator) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] "
+ "2f3b ] 3b 4b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actual = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actual);
+
+ // Pop open the folder.
+ BookmarkButton* button = [bar_ buttonWithTitleEqualTo:@"2f"];
+ [[button target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:button];
+ BookmarkBarFolderController* folder = [bar_ folderController];
+ EXPECT_TRUE(folder);
+
+ // Test a series of points starting at the top of the folder.
+ const CGFloat yOffset = 0.5 * bookmarks::kBookmarkVerticalPadding;
+ BookmarkButton* targetButton = [folder buttonWithTitleEqualTo:@"2f1b"];
+ ASSERT_TRUE(targetButton);
+ NSPoint targetPoint = [targetButton top];
+ CGFloat pos = [folder indicatorPosForDragToPoint:targetPoint];
+ EXPECT_CGFLOAT_EQ(targetPoint.y + yOffset, pos);
+ pos = [folder indicatorPosForDragToPoint:[targetButton bottom]];
+ targetButton = [folder buttonWithTitleEqualTo:@"2f2f"];
+ EXPECT_CGFLOAT_EQ([targetButton top].y + yOffset, pos);
+ pos = [folder indicatorPosForDragToPoint:NSMakePoint(10,0)];
+ targetButton = [folder buttonWithTitleEqualTo:@"2f3b"];
+ EXPECT_CGFLOAT_EQ([targetButton bottom].y - yOffset, pos);
+}
+
+@interface BookmarkBarControllerNoDelete : BookmarkBarController
+- (IBAction)deleteBookmark:(id)sender;
+@end
+
+@implementation BookmarkBarControllerNoDelete
+- (IBAction)deleteBookmark:(id)sender {
+ // NOP
+}
+@end
+
+class BookmarkBarFolderControllerClosingTest : public
+ BookmarkBarFolderControllerMenuTest {
+ public:
+ BookmarkBarFolderControllerClosingTest() {
+ bar_.reset([[BookmarkBarControllerNoDelete alloc]
+ initWithBrowser:helper_.browser()
+ initialWidth:NSWidth([parent_view_ frame])
+ delegate:nil
+ resizeDelegate:resizeDelegate_.get()]);
+ InstallAndToggleBar(bar_.get());
+ }
+};
+
+TEST_F(BookmarkBarFolderControllerClosingTest, DeleteClosesFolder) {
+ BookmarkModel& model(*helper_.profile()->GetBookmarkModel());
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b ] "
+ "2f3b ] 3b ");
+ model_test_utils::AddNodesFromModelString(model, root, model_string);
+
+ // Validate initial model.
+ std::string actualModelString = model_test_utils::ModelStringFromNode(root);
+ EXPECT_EQ(model_string, actualModelString);
+
+ // Open the folder menu and submenu.
+ BookmarkButton* target = [bar_ buttonWithTitleEqualTo:@"2f"];
+ ASSERT_TRUE(target);
+ [[target target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:target];
+ BookmarkBarFolderController* folder = [bar_ folderController];
+ EXPECT_TRUE(folder);
+ BookmarkButton* subTarget = [folder buttonWithTitleEqualTo:@"2f2f"];
+ ASSERT_TRUE(subTarget);
+ [[subTarget target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:subTarget];
+ BookmarkBarFolderController* subFolder = [folder folderController];
+ EXPECT_TRUE(subFolder);
+
+ // Delete the folder node and verify the window closed down by looking
+ // for its controller again.
+ [folder deleteBookmark:folder];
+ EXPECT_FALSE([folder 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/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h
new file mode 100644
index 0000000..373e0e6
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h
@@ -0,0 +1,78 @@
+// 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/ui/cocoa/bookmarks/bookmark_button.h"
+
+// Hover state machine. Encapsulates the hover state for
+// BookmarkBarFolderController.
+// A strict call order is implied with these calls. It is ONLY valid to make
+// the following state transitions:
+// From: To: Via:
+// closed opening scheduleOpen...:
+// opening closed cancelPendingOpen...: or
+// open scheduleOpen...: completes.
+// open closing scheduleClose...:
+// closing open cancelPendingClose...: or
+// closed scheduleClose...: completes.
+//
+@interface BookmarkBarFolderHoverState : NSObject {
+ @private
+ // Enumeration of the valid states that the |hoverButton_| member can be in.
+ // Because the opening and closing of hover views can be done asyncronously
+ // there are periods where the hover state is in transtion between open and
+ // closed. During those times of transition the opening or closing operation
+ // can be cancelled. We serialize the opening and closing of the
+ // |hoverButton_| using this state information. This serialization is to
+ // avoid race conditions where one hover button is being opened while another
+ // is closing.
+ enum HoverState {
+ kHoverStateClosed = 0,
+ kHoverStateOpening = 1,
+ kHoverStateOpen = 2,
+ kHoverStateClosing = 3
+ };
+
+ // 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_;
+
+ // We model hover state as a state machine with specific allowable
+ // transitions. |hoverState_| is the state of this machine at any
+ // given time.
+ HoverState hoverState_;
+}
+
+// Designated initializer.
+- (id)init;
+
+// The BookmarkBarFolderHoverState decides when it is appropriate to hide
+// and show the button that the BookmarkBarFolderController drags over.
+- (NSDragOperation)draggingEnteredButton:(BookmarkButton*)button;
+
+// The BookmarkBarFolderHoverState decides the fate of the hover button
+// when the BookmarkBarFolderController's view is exited.
+- (void)draggingExited;
+
+@end
+
+// Exposing these for unit testing purposes. They are used privately in the
+// implementation as well.
+@interface BookmarkBarFolderHoverState(PrivateAPI)
+// State change APIs.
+- (void)scheduleCloseBookmarkFolderOnHoverButton;
+- (void)cancelPendingCloseBookmarkFolderOnHoverButton;
+- (void)scheduleOpenBookmarkFolderOnHoverButton:(BookmarkButton*)hoverButton;
+- (void)cancelPendingOpenBookmarkFolderOnHoverButton;
+@end
+
+// Exposing these for unit testing purposes. They are used only in tests.
+@interface BookmarkBarFolderHoverState(TestingAPI)
+// Accessors and setters for button and hover state.
+- (BookmarkButton*)hoverButton;
+- (HoverState)hoverState;
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.mm
new file mode 100644
index 0000000..b762bb3c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.mm
@@ -0,0 +1,171 @@
+// 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/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+
+@interface BookmarkBarFolderHoverState(Private)
+- (void)setHoverState:(HoverState)state;
+- (void)closeBookmarkFolderOnHoverButton:(BookmarkButton*)button;
+- (void)openBookmarkFolderOnHoverButton:(BookmarkButton*)button;
+@end
+
+@implementation BookmarkBarFolderHoverState
+
+- (id)init {
+ if ((self = [super init])) {
+ hoverState_ = kHoverStateClosed;
+ }
+ return self;
+}
+
+- (NSDragOperation)draggingEnteredButton:(BookmarkButton*)button {
+ if ([button isFolder]) {
+ if (hoverButton_ == button) {
+ // CASE A: hoverButton_ == button implies we've dragged over
+ // the same folder so no need to open or close anything new.
+ } else if (hoverButton_ &&
+ hoverButton_ != button) {
+ // CASE B: we have a hoverButton_ but it is different from the new button.
+ // This implies we've dragged over a new folder, so we'll close the old
+ // and open the new.
+ // Note that we only schedule the open or close if we have no other tasks
+ // currently pending.
+
+ if (hoverState_ == kHoverStateOpen) {
+ // Close the old.
+ [self scheduleCloseBookmarkFolderOnHoverButton];
+ } else if (hoverState_ == kHoverStateClosed) {
+ // Open the new.
+ [self scheduleOpenBookmarkFolderOnHoverButton:button];
+ }
+ } else if (!hoverButton_) {
+ // CASE C: we don't have a current hoverButton_ but we have dragged onto
+ // a new folder so we open the new one.
+ [self scheduleOpenBookmarkFolderOnHoverButton:button];
+ }
+ } else if (!button) {
+ if (hoverButton_) {
+ // CASE D: We have a hoverButton_ but we've moved onto an area that
+ // requires no hover. We close the hoverButton_ in this case. This
+ // means cancelling if the open is pending (i.e. |kHoverStateOpening|)
+ // or closing if we don't alrealy have once in progress.
+
+ // Intiate close only if we have not already done so.
+ if (hoverState_ == kHoverStateOpening) {
+ // Cancel the pending open.
+ [self cancelPendingOpenBookmarkFolderOnHoverButton];
+ } else if (hoverState_ != kHoverStateClosing) {
+ // Schedule the close.
+ [self scheduleCloseBookmarkFolderOnHoverButton];
+ }
+ } else {
+ // CASE E: We have neither a hoverButton_ nor a new button that requires
+ // a hover. In this case we do nothing.
+ }
+ }
+
+ return NSDragOperationMove;
+}
+
+- (void)draggingExited {
+ if (hoverButton_) {
+ if (hoverState_ == kHoverStateOpening) {
+ [self cancelPendingOpenBookmarkFolderOnHoverButton];
+ } else if (hoverState_ == kHoverStateClosing) {
+ [self cancelPendingCloseBookmarkFolderOnHoverButton];
+ }
+ }
+}
+
+// Schedule close of hover button. Transition to kHoverStateClosing state.
+- (void)scheduleCloseBookmarkFolderOnHoverButton {
+ DCHECK(hoverButton_);
+ [self setHoverState:kHoverStateClosing];
+ [self performSelector:@selector(closeBookmarkFolderOnHoverButton:)
+ withObject:hoverButton_
+ afterDelay:bookmarks::kDragHoverCloseDelay];
+}
+
+// Cancel pending hover close. Transition to kHoverStateOpen state.
+- (void)cancelPendingCloseBookmarkFolderOnHoverButton {
+ [self setHoverState:kHoverStateOpen];
+ [NSObject
+ cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(closeBookmarkFolderOnHoverButton:)
+ object:hoverButton_];
+}
+
+// Schedule open of hover button. Transition to kHoverStateOpening state.
+- (void)scheduleOpenBookmarkFolderOnHoverButton:(BookmarkButton*)button {
+ DCHECK(button);
+ hoverButton_.reset([button retain]);
+ [self setHoverState:kHoverStateOpening];
+ [self performSelector:@selector(openBookmarkFolderOnHoverButton:)
+ withObject:hoverButton_
+ afterDelay:bookmarks::kDragHoverOpenDelay];
+}
+
+// Cancel pending hover open. Transition to kHoverStateClosed state.
+- (void)cancelPendingOpenBookmarkFolderOnHoverButton {
+ [self setHoverState:kHoverStateClosed];
+ [NSObject
+ cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(openBookmarkFolderOnHoverButton:)
+ object:hoverButton_];
+ hoverButton_.reset();
+}
+
+// Hover button accessor. For testing only.
+- (BookmarkButton*)hoverButton {
+ return hoverButton_;
+}
+
+// Hover state accessor. For testing only.
+- (HoverState)hoverState {
+ return hoverState_;
+}
+
+// This method encodes the rules of our |hoverButton_| state machine. Only
+// specific state transitions are allowable (encoded in the DCHECK).
+// Note that there is no state for simultaneously opening and closing. A
+// pending open must complete before scheduling a close, and vice versa. And
+// it is not possible to make a transition directly from open to closed, and
+// vice versa.
+- (void)setHoverState:(HoverState)state {
+ DCHECK(
+ (hoverState_ == kHoverStateClosed && state == kHoverStateOpening) ||
+ (hoverState_ == kHoverStateOpening && state == kHoverStateClosed) ||
+ (hoverState_ == kHoverStateOpening && state == kHoverStateOpen) ||
+ (hoverState_ == kHoverStateOpen && state == kHoverStateClosing) ||
+ (hoverState_ == kHoverStateClosing && state == kHoverStateOpen) ||
+ (hoverState_ == kHoverStateClosing && state == kHoverStateClosed)
+ ) << "bad transition: old = " << hoverState_ << " new = " << state;
+
+ hoverState_ = state;
+}
+
+// Called after a delay to close a previously hover-opened folder.
+// Note: this method is not meant to be invoked directly, only through
+// a delayed call to |scheduleCloseBookmarkFolderOnHoverButton:|.
+- (void)closeBookmarkFolderOnHoverButton:(BookmarkButton*)button {
+ [NSObject
+ cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(closeBookmarkFolderOnHoverButton:)
+ object:hoverButton_];
+ [self setHoverState:kHoverStateClosed];
+ [[button target] closeBookmarkFolder:button];
+ hoverButton_.reset();
+}
+
+// Called after a delay to open a new hover folder.
+// Note: this method is not meant to be invoked directly, only through
+// a delayed call to |scheduleOpenBookmarkFolderOnHoverButton:|.
+- (void)openBookmarkFolderOnHoverButton:(BookmarkButton*)button {
+ [self setHoverState:kHoverStateOpen];
+ [[button target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:button];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state_unittest.mm
new file mode 100644
index 0000000..3d0a50f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state_unittest.mm
@@ -0,0 +1,77 @@
+// 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/message_loop.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h"
+#import "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+
+namespace {
+
+typedef CocoaTest BookmarkBarFolderHoverStateTest;
+
+// Hover state machine interface.
+// A strict call order is implied with these calls. It is ONLY valid to make
+// these specific state transitions.
+TEST(BookmarkBarFolderHoverStateTest, HoverState) {
+ BrowserTestHelper helper;
+ scoped_nsobject<BookmarkBarFolderHoverState> bbfhs;
+ bbfhs.reset([[BookmarkBarFolderHoverState alloc] init]);
+
+ // Initial state.
+ EXPECT_FALSE([bbfhs hoverButton]);
+ ASSERT_EQ(kHoverStateClosed, [bbfhs hoverState]);
+
+ scoped_nsobject<BookmarkButton> button;
+ button.reset([[BookmarkButton alloc] initWithFrame:NSMakeRect(0, 0, 20, 20)]);
+
+ // Test transition from closed to opening.
+ ASSERT_EQ(kHoverStateClosed, [bbfhs hoverState]);
+ [bbfhs scheduleOpenBookmarkFolderOnHoverButton:button];
+ ASSERT_EQ(kHoverStateOpening, [bbfhs hoverState]);
+
+ // Test transition from opening to closed (aka cancel open).
+ [bbfhs cancelPendingOpenBookmarkFolderOnHoverButton];
+ ASSERT_EQ(kHoverStateClosed, [bbfhs hoverState]);
+ ASSERT_EQ(nil, [bbfhs hoverButton]);
+
+ // Test transition from closed to opening.
+ ASSERT_EQ(kHoverStateClosed, [bbfhs hoverState]);
+ [bbfhs scheduleOpenBookmarkFolderOnHoverButton:button];
+ ASSERT_EQ(kHoverStateOpening, [bbfhs hoverState]);
+
+ // Test transition from opening to opened.
+ MessageLoop::current()->PostDelayedTask(
+ FROM_HERE,
+ new MessageLoop::QuitTask,
+ bookmarks::kDragHoverOpenDelay * 1000.0 * 1.5);
+ MessageLoop::current()->Run();
+ ASSERT_EQ(kHoverStateOpen, [bbfhs hoverState]);
+ ASSERT_EQ(button, [bbfhs hoverButton]);
+
+ // Test transition from opening to opened.
+ [bbfhs scheduleCloseBookmarkFolderOnHoverButton];
+ ASSERT_EQ(kHoverStateClosing, [bbfhs hoverState]);
+
+ // Test transition from closing to open (aka cancel close).
+ [bbfhs cancelPendingCloseBookmarkFolderOnHoverButton];
+ ASSERT_EQ(kHoverStateOpen, [bbfhs hoverState]);
+ ASSERT_EQ(button, [bbfhs hoverButton]);
+
+ // Test transition from closing to closed.
+ [bbfhs scheduleCloseBookmarkFolderOnHoverButton];
+ ASSERT_EQ(kHoverStateClosing, [bbfhs hoverState]);
+ MessageLoop::current()->PostDelayedTask(
+ FROM_HERE,
+ new MessageLoop::QuitTask,
+ bookmarks::kDragHoverCloseDelay * 1000.0 * 1.5);
+ MessageLoop::current()->Run();
+ ASSERT_EQ(kHoverStateClosed, [bbfhs hoverState]);
+ ASSERT_EQ(nil, [bbfhs hoverButton]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h
new file mode 100644
index 0000000..8f60b8e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h
@@ -0,0 +1,29 @@
+// 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>
+
+@protocol BookmarkButtonControllerProtocol;
+@class BookmarkBarFolderController;
+
+// Main content view for a bookmark bar folder "menu" window. This is
+// logically similar to a BookmarkBarView but is oriented vertically.
+@interface BookmarkBarFolderView : NSView {
+ @private
+ BOOL inDrag_; // Are we in the middle of a drag?
+ BOOL dropIndicatorShown_;
+ CGFloat dropIndicatorPosition_; // y position
+ // The following |controller_| is weak; used for testing only. See the imple-
+ // mentation comment for - (id<BookmarkButtonControllerProtocol>)controller.
+ BookmarkBarFolderController* controller_;
+}
+// Return the controller that owns this view.
+- (id<BookmarkButtonControllerProtocol>)controller;
+@end
+
+@interface BookmarkBarFolderView() // TestingOrInternalAPI
+@property (assign) BOOL dropIndicatorShown;
+@property (readonly) CGFloat dropIndicatorPosition;
+- (void)setController:(id)controller;
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.mm
new file mode 100644
index 0000000..5a451a8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.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 "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h"
+
+#include "chrome/browser/bookmarks/bookmark_pasteboard_helper_mac.h"
+#include "chrome/browser/metrics/user_metrics.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h"
+#import "third_party/mozilla/NSPasteboard+Utils.h"
+
+@implementation BookmarkBarFolderView
+
+@synthesize dropIndicatorShown = dropIndicatorShown_;
+@synthesize dropIndicatorPosition = dropIndicatorPosition_;
+
+- (void)awakeFromNib {
+ NSArray* types = [NSArray arrayWithObjects:
+ NSStringPboardType,
+ NSHTMLPboardType,
+ NSURLPboardType,
+ kBookmarkButtonDragType,
+ kBookmarkDictionaryListPboardType,
+ nil];
+ [self registerForDraggedTypes:types];
+}
+
+- (void)dealloc {
+ [self unregisterDraggedTypes];
+ [super dealloc];
+}
+
+- (id<BookmarkButtonControllerProtocol>)controller {
+ // When needed for testing, set the local data member |controller_| to
+ // the test controller.
+ return controller_ ? controller_ : [[self window] windowController];
+}
+
+- (void)setController:(id)controller {
+ controller_ = controller;
+}
+
+- (void)drawRect:(NSRect)rect {
+ // 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);
+ NSColor* uglyBlackBarColor = [NSColor blackColor];
+ [[uglyBlackBarColor colorWithAlphaComponent:kBarOpacity] setFill];
+ [[NSBezierPath bezierPathWithRect:uglyBlackBar] fill];
+ }
+}
+
+// TODO(mrossetti,jrg): Identical to -[BookmarkBarView
+// dragClipboardContainsBookmarks]. http://crbug.com/35966
+// Shim function to assist in unit testing.
+- (BOOL)dragClipboardContainsBookmarks {
+ return bookmark_pasteboard_helper_mac::DragClipboardContainsBookmarks();
+}
+
+// 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;
+ if ([[info draggingPasteboard] dataForType:kBookmarkButtonDragType] ||
+ [self dragClipboardContainsBookmarks] ||
+ [[info draggingPasteboard] containsURLData]) {
+ // Find the position of the drop indicator.
+ BOOL showIt = [[self controller]
+ shouldShowIndicatorShownForPoint:[info draggingLocation]];
+ if (!showIt) {
+ if (dropIndicatorShown_) {
+ dropIndicatorShown_ = NO;
+ [self setNeedsDisplay:YES];
+ }
+ } else {
+ CGFloat y =
+ [[self controller]
+ indicatorPosForDragToPoint:[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 [info draggingSource] ? NSDragOperationMove : NSDragOperationCopy;
+ }
+ 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;
+ }
+
+ [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;
+}
+
+// This code is practically identical to the same function in BookmarkBarView
+// with the only difference being how the controller is retrieved.
+// TODO(mrossetti,jrg): http://crbug.com/35966
+// Implement NSDraggingDestination protocol method
+// performDragOperation: for URLs.
+- (BOOL)performDragOperationForURL:(id<NSDraggingInfo>)info {
+ NSPasteboard* pboard = [info draggingPasteboard];
+ DCHECK([pboard containsURLData]);
+
+ NSArray* urls = nil;
+ NSArray* titles = nil;
+ [pboard getURLs:&urls andTitles:&titles convertingFilenames:YES];
+
+ return [[self controller] addURLs:urls
+ withTitles:titles
+ at:[info draggingLocation]];
+}
+
+// This code is practically identical to the same function in BookmarkBarView
+// with the only difference being how the controller is retrieved.
+// http://crbug.com/35966
+// Implement NSDraggingDestination protocol method
+// performDragOperation: for bookmark buttons.
+- (BOOL)performDragOperationForBookmarkButton:(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)];
+ BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove);
+ doDrag = [[self controller] dragButton:button
+ to:[info draggingLocation]
+ copy:copy];
+ UserMetrics::RecordAction(UserMetricsAction("BookmarkBarFolder_DragEnd"));
+ }
+ return doDrag;
+}
+
+- (BOOL)performDragOperation:(id<NSDraggingInfo>)info {
+ if ([[self controller] dragBookmarkData:info])
+ return YES;
+ NSPasteboard* pboard = [info draggingPasteboard];
+ if ([pboard dataForType:kBookmarkButtonDragType] &&
+ [self performDragOperationForBookmarkButton:info])
+ return YES;
+ if ([pboard containsURLData] && [self performDragOperationForURL:info])
+ return YES;
+ return NO;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view_unittest.mm
new file mode 100644
index 0000000..07aca2b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view_unittest.mm
@@ -0,0 +1,211 @@
+// 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/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/url_drop_target.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+#import "third_party/mozilla/NSPasteboard+Utils.h"
+
+namespace {
+ const CGFloat kFakeIndicatorPos = 7.0;
+};
+
+// Fake DraggingInfo, fake BookmarkBarController, fake NSPasteboard...
+@interface FakeDraggingInfo : NSObject {
+ @public
+ BOOL dragButtonToPong_;
+ BOOL dragURLsPong_;
+ BOOL dragBookmarkDataPong_;
+ BOOL dropIndicatorShown_;
+ BOOL draggingEnteredCalled_;
+ // Only mock one type of drag data at a time.
+ NSString* dragDataType_;
+}
+@property (readwrite) BOOL dropIndicatorShown;
+@property (readwrite) BOOL draggingEnteredCalled;
+@property (copy) NSString* dragDataType;
+@end
+
+@implementation FakeDraggingInfo
+
+@synthesize dropIndicatorShown = dropIndicatorShown_;
+@synthesize draggingEnteredCalled = draggingEnteredCalled_;
+@synthesize dragDataType = dragDataType_;
+
+- (id)init {
+ if ((self = [super init])) {
+ dropIndicatorShown_ = YES;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [dragDataType_ release];
+ [super dealloc];
+}
+
+- (void)reset {
+ [dragDataType_ release];
+ dragDataType_ = nil;
+ dragButtonToPong_ = NO;
+ dragURLsPong_ = NO;
+ dragBookmarkDataPong_ = NO;
+ dropIndicatorShown_ = YES;
+ draggingEnteredCalled_ = NO;
+}
+
+// NSDragInfo mocking functions.
+
+- (id)draggingPasteboard {
+ return self;
+}
+
+// So we can look local.
+- (id)draggingSource {
+ return self;
+}
+
+- (NSDragOperation)draggingSourceOperationMask {
+ return NSDragOperationCopy | NSDragOperationMove;
+}
+
+- (NSPoint)draggingLocation {
+ return NSMakePoint(10, 10);
+}
+
+// NSPasteboard mocking functions.
+
+- (BOOL)containsURLData {
+ NSArray* urlTypes = [URLDropTargetHandler handledDragTypes];
+ if (dragDataType_)
+ return [urlTypes containsObject:dragDataType_];
+ return NO;
+}
+
+- (NSData*)dataForType:(NSString*)type {
+ if (dragDataType_ && [dragDataType_ isEqualToString:type])
+ return [NSData data]; // Return something, anything.
+ return nil;
+}
+
+// Fake a controller for callback ponging
+
+- (BOOL)dragButton:(BookmarkButton*)button to:(NSPoint)point copy:(BOOL)copy {
+ dragButtonToPong_ = YES;
+ return YES;
+}
+
+- (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point {
+ dragURLsPong_ = YES;
+ return YES;
+}
+
+- (void)getURLs:(NSArray**)outUrls
+ andTitles:(NSArray**)outTitles
+ convertingFilenames:(BOOL)convertFilenames {
+}
+
+- (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info {
+ dragBookmarkDataPong_ = YES;
+ return NO;
+}
+
+// Confirm the pongs.
+
+- (BOOL)dragButtonToPong {
+ return dragButtonToPong_;
+}
+
+- (BOOL)dragURLsPong {
+ return dragURLsPong_;
+}
+
+- (BOOL)dragBookmarkDataPong {
+ return dragBookmarkDataPong_;
+}
+
+- (CGFloat)indicatorPosForDragToPoint:(NSPoint)point {
+ return kFakeIndicatorPos;
+}
+
+- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point {
+ return dropIndicatorShown_;
+}
+
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info {
+ draggingEnteredCalled_ = YES;
+ return NSDragOperationNone;
+}
+
+@end
+
+namespace {
+
+class BookmarkBarFolderViewTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ view_.reset([[BookmarkBarFolderView alloc] init]);
+ }
+
+ scoped_nsobject<BookmarkBarFolderView> view_;
+};
+
+TEST_F(BookmarkBarFolderViewTest, BookmarkButtonDragAndDrop) {
+ [view_ awakeFromNib];
+ scoped_nsobject<FakeDraggingInfo> info([[FakeDraggingInfo alloc] init]);
+ [view_ setController:info.get()];
+ [info reset];
+
+ [info setDragDataType:kBookmarkButtonDragType];
+ EXPECT_EQ([view_ draggingEntered:(id)info.get()], NSDragOperationMove);
+ EXPECT_TRUE([view_ performDragOperation:(id)info.get()]);
+ EXPECT_TRUE([info dragButtonToPong]);
+ EXPECT_FALSE([info dragURLsPong]);
+ EXPECT_TRUE([info dragBookmarkDataPong]);
+}
+
+TEST_F(BookmarkBarFolderViewTest, URLDragAndDrop) {
+ [view_ awakeFromNib];
+ scoped_nsobject<FakeDraggingInfo> info([[FakeDraggingInfo alloc] init]);
+ [view_ setController:info.get()];
+ [info reset];
+
+ NSArray* dragTypes = [URLDropTargetHandler handledDragTypes];
+ for (NSString* type in dragTypes) {
+ [info setDragDataType:type];
+ EXPECT_EQ([view_ draggingEntered:(id)info.get()], NSDragOperationMove);
+ EXPECT_TRUE([view_ performDragOperation:(id)info.get()]);
+ EXPECT_FALSE([info dragButtonToPong]);
+ EXPECT_TRUE([info dragURLsPong]);
+ EXPECT_TRUE([info dragBookmarkDataPong]);
+ [info reset];
+ }
+}
+
+TEST_F(BookmarkBarFolderViewTest, BookmarkButtonDropIndicator) {
+ [view_ awakeFromNib];
+ scoped_nsobject<FakeDraggingInfo> info([[FakeDraggingInfo alloc] init]);
+ [view_ setController:info.get()];
+ [info reset];
+
+ [info setDragDataType:kBookmarkButtonDragType];
+ EXPECT_FALSE([info draggingEnteredCalled]);
+ EXPECT_EQ([view_ draggingEntered:(id)info.get()], NSDragOperationMove);
+ EXPECT_TRUE([info draggingEnteredCalled]); // Ensure controller pinged.
+ 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/ui/cocoa/bookmarks/bookmark_bar_folder_window.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h
new file mode 100644
index 0000000..1b80d91
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h
@@ -0,0 +1,34 @@
+// 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_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_FOLDER_WINDOW_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_FOLDER_WINDOW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.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
+
+// Content view for the above window. "Stock" other than the drawing
+// of rounded corners. Only used in the nib.
+@interface BookmarkBarFolderWindowContentView : NSView {
+ // Arrows to show ability to scroll up and down as needed.
+ scoped_nsobject<NSImage> arrowUpImage_;
+ scoped_nsobject<NSImage> arrowDownImage_;
+}
+@end
+
+// Scroll view that contains the main view (where the buttons go).
+@interface BookmarkBarFolderWindowScrollView : NSScrollView
+@end
+
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_FOLDER_WINDOW_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.mm
new file mode 100644
index 0000000..0915188
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.mm
@@ -0,0 +1,136 @@
+// 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/ui/cocoa/bookmarks/bookmark_bar_folder_window.h"
+
+#import "base/logging.h"
+#include "base/nsimage_cache_mac.h"
+#import "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h"
+#import "chrome/browser/ui/cocoa/image_utils.h"
+#import "third_party/GTM/AppKit/GTMNSColor+Luminance.h"
+#import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h"
+
+
+@implementation BookmarkBarFolderWindow
+
+- (id)initWithContentRect:(NSRect)contentRect
+ styleMask:(NSUInteger)windowStyle
+ backing:(NSBackingStoreType)bufferingType
+ defer:(BOOL)deferCreation {
+ if ((self = [super initWithContentRect:contentRect
+ styleMask:NSBorderlessWindowMask // override
+ backing:bufferingType
+ defer:deferCreation])) {
+ [self setBackgroundColor:[NSColor clearColor]];
+ [self setOpaque:NO];
+ }
+ return self;
+}
+
+@end
+
+
+namespace {
+// Corner radius for our bookmark bar folder window.
+// Copied from bubble_view.mm.
+const CGFloat kViewCornerRadius = 4.0;
+}
+
+@implementation BookmarkBarFolderWindowContentView
+
+- (void)awakeFromNib {
+ arrowUpImage_.reset([nsimage_cache::ImageNamed(@"menu_overflow_up.pdf")
+ retain]);
+ arrowDownImage_.reset([nsimage_cache::ImageNamed(@"menu_overflow_down.pdf")
+ retain]);
+}
+
+// Draw the arrows at the top and bottom of the folder window as a
+// visual indication that scrolling is possible. We always draw the
+// scrolling arrows; when not relevant (e.g. when not scrollable), the
+// scroll view overlaps the window and the arrows aren't visible.
+- (void)drawScrollArrows:(NSRect)rect {
+ NSRect visibleRect = [self bounds];
+
+ // On top
+ NSRect imageRect = NSZeroRect;
+ imageRect.size = [arrowUpImage_ size];
+ NSRect drawRect = NSOffsetRect(
+ imageRect,
+ (NSWidth(visibleRect) - NSWidth(imageRect)) / 2,
+ NSHeight(visibleRect) - NSHeight(imageRect));
+ [arrowUpImage_ drawInRect:drawRect
+ fromRect:imageRect
+ operation:NSCompositeSourceOver
+ fraction:1.0
+ neverFlipped:YES];
+
+ // On bottom
+ imageRect = NSZeroRect;
+ imageRect.size = [arrowDownImage_ size];
+ drawRect = NSOffsetRect(imageRect,
+ (NSWidth(visibleRect) - NSWidth(imageRect)) / 2,
+ 0);
+ [arrowDownImage_ drawInRect:drawRect
+ fromRect:imageRect
+ operation:NSCompositeSourceOver
+ fraction:1.0
+ neverFlipped:YES];
+}
+
+- (void)drawRect:(NSRect)rect {
+ NSRect bounds = [self bounds];
+ // Like NSMenus, only the bottom corners are rounded.
+ NSBezierPath* bezier =
+ [NSBezierPath gtm_bezierPathWithRoundRect:bounds
+ topLeftCornerRadius:0
+ topRightCornerRadius:0
+ bottomLeftCornerRadius:kViewCornerRadius
+ bottomRightCornerRadius:kViewCornerRadius];
+ [bezier closePath];
+
+ // TODO(jrg): share code with info_bubble_view.mm? Or bubble_view.mm?
+ NSColor* base_color = [NSColor colorWithCalibratedWhite:0.5 alpha:1.0];
+ NSColor* startColor =
+ [base_color gtm_colorAdjustedFor:GTMColorationLightHighlight
+ faded:YES];
+ NSColor* midColor =
+ [base_color gtm_colorAdjustedFor:GTMColorationLightMidtone
+ faded:YES];
+ NSColor* endColor =
+ [base_color gtm_colorAdjustedFor:GTMColorationLightShadow
+ faded:YES];
+ NSColor* glowColor =
+ [base_color gtm_colorAdjustedFor:GTMColorationLightPenumbra
+ faded:YES];
+
+ scoped_nsobject<NSGradient> gradient(
+ [[NSGradient alloc] initWithColorsAndLocations:startColor, 0.0,
+ midColor, 0.25,
+ endColor, 0.5,
+ glowColor, 0.75,
+ nil]);
+ [gradient drawInBezierPath:bezier angle:0.0];
+
+ [self drawScrollArrows:rect];
+}
+
+@end
+
+
+@implementation BookmarkBarFolderWindowScrollView
+
+// We want "draw background" of the NSScrollView in the xib to be NOT
+// checked. That allows us to round the bottom corners of the folder
+// window. However that also allows some scrollWheel: events to leak
+// into the NSWindow behind it (even in a different application).
+// Better to plug the scroll leak than to round corners for M5.
+- (void)scrollWheel:(NSEvent *)theEvent {
+ DCHECK([[[self window] windowController]
+ respondsToSelector:@selector(scrollWheel:)]);
+ [[[self window] windowController] scrollWheel:theEvent];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window_unittest.mm
new file mode 100644
index 0000000..7dd6c06
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window_unittest.mm
@@ -0,0 +1,49 @@
+// 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/ui/cocoa/bookmarks/bookmark_bar_folder_window.h"
+#include "chrome/browser/ui/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]);
+}
+
+
+class BookmarkBarFolderWindowContentViewTest : public CocoaTest {
+ public:
+ BookmarkBarFolderWindowContentViewTest() {
+ view_.reset([[BookmarkBarFolderWindowContentView alloc]
+ initWithFrame:NSMakeRect(0, 0, 100, 100)]);
+ [[test_window() contentView] addSubview:view_.get()];
+ }
+ scoped_nsobject<BookmarkBarFolderWindowContentView> view_;
+ scoped_nsobject<BookmarkBarFolderWindowScrollView> scroll_view_;
+};
+
+TEST_VIEW(BookmarkBarFolderWindowContentViewTest, view_);
+
+
+class BookmarkBarFolderWindowScrollViewTest : public CocoaTest {
+ public:
+ BookmarkBarFolderWindowScrollViewTest() {
+ scroll_view_.reset([[BookmarkBarFolderWindowScrollView alloc]
+ initWithFrame:NSMakeRect(0, 0, 100, 100)]);
+ [[test_window() contentView] addSubview:scroll_view_.get()];
+ }
+ scoped_nsobject<BookmarkBarFolderWindowScrollView> scroll_view_;
+};
+
+TEST_VIEW(BookmarkBarFolderWindowScrollViewTest, scroll_view_);
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_state.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_state.h
new file mode 100644
index 0000000..c21c75d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_state.h
@@ -0,0 +1,62 @@
+// 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_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_STATE_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_STATE_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+namespace bookmarks {
+
+// States for the bookmark bar.
+enum VisualState {
+ kInvalidState = 0,
+ kHiddenState = 1,
+ kShowingState = 2,
+ kDetachedState = 3,
+};
+
+} // namespace bookmarks
+
+// The interface for controllers (etc.) which can give information about the
+// bookmark bar's state.
+@protocol BookmarkBarState
+
+// Returns YES if the bookmark bar is currently visible (as a normal toolbar or
+// as a detached bar on the NTP), NO otherwise.
+- (BOOL)isVisible;
+
+// Returns YES if an animation is currently running, NO otherwise.
+- (BOOL)isAnimationRunning;
+
+// Returns YES if the bookmark bar is in the given state and not in an
+// animation, NO otherwise.
+- (BOOL)isInState:(bookmarks::VisualState)state;
+
+// Returns YES if the bookmark bar is animating from the given state (to any
+// other state), NO otherwise.
+- (BOOL)isAnimatingToState:(bookmarks::VisualState)state;
+
+// Returns YES if the bookmark bar is animating to the given state (from any
+// other state), NO otherwise.
+- (BOOL)isAnimatingFromState:(bookmarks::VisualState)state;
+
+// Returns YES if the bookmark bar is animating from the first given state to
+// the second given state, NO otherwise.
+- (BOOL)isAnimatingFromState:(bookmarks::VisualState)fromState
+ toState:(bookmarks::VisualState)toState;
+
+// Returns YES if the bookmark bar is animating between the two given states (in
+// either direction), NO otherwise.
+- (BOOL)isAnimatingBetweenState:(bookmarks::VisualState)fromState
+ andState:(bookmarks::VisualState)toState;
+
+// Returns how morphed into the detached bubble the bookmark bar should be (1 =
+// completely detached, 0 = normal).
+- (CGFloat)detachedMorphProgress;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_STATE_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h
new file mode 100644
index 0000000..1942ebd
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h
@@ -0,0 +1,44 @@
+// 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.
+
+// The BookmarkBarToolbarView is responsible for drawing the background of the
+// BookmarkBar's toolbar in either of its two display modes - permanently
+// attached (slimline with a stroke at the bottom edge) or New Tab Page style
+// (padded with a round rect border and the New Tab Page theme behind).
+
+#ifndef CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_TOOLBAR_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_TOOLBAR_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/animatable_view.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_state.h"
+
+@class BookmarkBarView;
+class TabContents;
+class ThemeProvider;
+
+// An interface to allow mocking of a BookmarkBarController by the
+// BookmarkBarToolbarView.
+@protocol BookmarkBarToolbarViewController <BookmarkBarState>
+// Displaying the bookmark toolbar background in bubble (floating) mode requires
+// the size of the currently selected tab to properly calculate where the
+// background image is joined.
+- (int)currentTabContentsHeight;
+
+// Current theme provider, passed to the cross platform NtpBackgroundUtil class.
+- (ThemeProvider*)themeProvider;
+
+@end
+
+@interface BookmarkBarToolbarView : AnimatableView {
+ @private
+ // The controller which tells us how we should be drawing (as normal or as a
+ // floating bar).
+ IBOutlet id<BookmarkBarToolbarViewController> controller_;
+}
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_TOOLBAR_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.mm
new file mode 100644
index 0000000..760de17
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.mm
@@ -0,0 +1,135 @@
+// 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/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h"
+
+#include "app/theme_provider.h"
+#include "gfx/rect.h"
+#include "chrome/browser/ntp_background_util.h"
+#include "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#include "gfx/canvas_skia_paint.h"
+
+const CGFloat kBorderRadius = 3.0;
+
+@interface BookmarkBarToolbarView (Private)
+- (void)drawRectAsBubble:(NSRect)rect;
+@end
+
+@implementation BookmarkBarToolbarView
+
+- (BOOL)isOpaque {
+ return [controller_ isInState:bookmarks::kDetachedState];
+}
+
+- (void)drawRect:(NSRect)rect {
+ if ([controller_ isInState:bookmarks::kDetachedState] ||
+ [controller_ isAnimatingToState:bookmarks::kDetachedState] ||
+ [controller_ isAnimatingFromState:bookmarks::kDetachedState]) {
+ [self drawRectAsBubble:rect];
+ } else {
+ NSPoint phase = [[self window] themePatternPhase];
+ [[NSGraphicsContext currentContext] setPatternPhase:phase];
+ [self drawBackground];
+ }
+}
+
+- (void)drawRectAsBubble:(NSRect)rect {
+ // The state of our morph; 1 is total bubble, 0 is the regular bar. We use it
+ // to morph the bubble to a regular bar (shape and colour).
+ CGFloat morph = [controller_ detachedMorphProgress];
+
+ NSRect bounds = [self bounds];
+
+ ThemeProvider* themeProvider = [controller_ themeProvider];
+ if (!themeProvider)
+ return;
+
+ NSGraphicsContext* context = [NSGraphicsContext currentContext];
+ [context saveGraphicsState];
+
+ // Draw the background.
+ {
+ // CanvasSkiaPaint draws to the NSGraphicsContext during its destructor, so
+ // explicitly scope this.
+ //
+ // Paint the entire bookmark bar, even if the damage rect is much smaller
+ // because PaintBackgroundDetachedMode() assumes that area's origin is
+ // (0, 0) and that its size is the size of the bookmark bar.
+ //
+ // In practice, this sounds worse than it is because redraw time is still
+ // minimal compared to the pause between frames of animations. We were
+ // already repainting the rest of the bookmark bar below without setting a
+ // clip area, anyway. Also, the only time we weren't asked to redraw the
+ // whole bookmark bar is when the find bar is drawn over it.
+ gfx::CanvasSkiaPaint canvas(bounds, true);
+ gfx::Rect area(0, 0, NSWidth(bounds), NSHeight(bounds));
+
+ NtpBackgroundUtil::PaintBackgroundDetachedMode(themeProvider, &canvas,
+ area, [controller_ currentTabContentsHeight]);
+ }
+
+ // Draw our bookmark bar border on top of the background.
+ NSRect frameRect =
+ NSMakeRect(
+ morph * bookmarks::kNTPBookmarkBarPadding,
+ morph * bookmarks::kNTPBookmarkBarPadding,
+ NSWidth(bounds) - 2 * morph * bookmarks::kNTPBookmarkBarPadding,
+ NSHeight(bounds) - 2 * morph * bookmarks::kNTPBookmarkBarPadding);
+ // Now draw a bezier path with rounded rectangles around the area.
+ frameRect = NSInsetRect(frameRect, morph * 0.5, morph * 0.5);
+ NSBezierPath* border =
+ [NSBezierPath bezierPathWithRoundedRect:frameRect
+ xRadius:(morph * kBorderRadius)
+ yRadius:(morph * kBorderRadius)];
+
+ // Draw the rounded rectangle.
+ NSColor* toolbarColor =
+ themeProvider->GetNSColor(BrowserThemeProvider::COLOR_TOOLBAR, true);
+ CGFloat alpha = morph * [toolbarColor alphaComponent];
+ [[toolbarColor colorWithAlphaComponent:alpha] set]; // Set with opacity.
+ [border fill];
+
+ // Fade in/out the background.
+ [context saveGraphicsState];
+ [border setClip];
+ CGContextRef cgContext = (CGContextRef)[context graphicsPort];
+ CGContextBeginTransparencyLayer(cgContext, NULL);
+ CGContextSetAlpha(cgContext, 1 - morph);
+ [context setPatternPhase:[[self window] themePatternPhase]];
+ [self drawBackground];
+ CGContextEndTransparencyLayer(cgContext);
+ [context restoreGraphicsState];
+
+ // Draw the border of the rounded rectangle.
+ NSColor* borderColor = themeProvider->GetNSColor(
+ BrowserThemeProvider::COLOR_TOOLBAR_BUTTON_STROKE, true);
+ alpha = morph * [borderColor alphaComponent];
+ [[borderColor colorWithAlphaComponent:alpha] set]; // Set with opacity.
+ [border stroke];
+
+ // Fade in/out the divider.
+ // TODO(viettrungluu): It's not obvious that this divider lines up exactly
+ // with |BackgroundGradientView|'s (in fact, it probably doesn't).
+ NSColor* strokeColor = [self strokeColor];
+ alpha = (1 - morph) * [strokeColor alphaComponent];
+ [[strokeColor colorWithAlphaComponent:alpha] set];
+ NSBezierPath* divider = [NSBezierPath bezierPath];
+ NSPoint dividerStart =
+ NSMakePoint(morph * bookmarks::kNTPBookmarkBarPadding + morph * 0.5,
+ morph * bookmarks::kNTPBookmarkBarPadding + morph * 0.5);
+ CGFloat dividerWidth =
+ NSWidth(bounds) - 2 * morph * bookmarks::kNTPBookmarkBarPadding - 2 * 0.5;
+ [divider moveToPoint:dividerStart];
+ [divider relativeLineToPoint:NSMakePoint(dividerWidth, 0)];
+ [divider stroke];
+
+ // Restore the graphics context.
+ [context restoreGraphicsState];
+}
+
+@end // @implementation BookmarkBarToolbarView
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view_unittest.mm
new file mode 100644
index 0000000..24d971a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view_unittest.mm
@@ -0,0 +1,191 @@
+// 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 "app/theme_provider.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "grit/theme_resources.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#include "third_party/skia/include/core/SkColor.h"
+
+using ::testing::_;
+using ::testing::DoAll;
+using ::testing::Return;
+using ::testing::SetArgumentPointee;
+
+// When testing the floating drawing, we need to have a source of theme data.
+class MockThemeProvider : public ThemeProvider {
+ public:
+ // Cross platform methods
+ MOCK_METHOD1(Init, void(Profile*));
+ MOCK_CONST_METHOD1(GetBitmapNamed, SkBitmap*(int));
+ MOCK_CONST_METHOD1(GetColor, SkColor(int));
+ MOCK_CONST_METHOD2(GetDisplayProperty, bool(int, int*));
+ MOCK_CONST_METHOD0(ShouldUseNativeFrame, bool());
+ MOCK_CONST_METHOD1(HasCustomImage, bool(int));
+ MOCK_CONST_METHOD1(GetRawData, RefCountedMemory*(int));
+
+ // OSX stuff
+ MOCK_CONST_METHOD2(GetNSImageNamed, NSImage*(int, bool));
+ MOCK_CONST_METHOD2(GetNSImageColorNamed, NSColor*(int, bool));
+ MOCK_CONST_METHOD2(GetNSColor, NSColor*(int, bool));
+ MOCK_CONST_METHOD2(GetNSColorTint, NSColor*(int, bool));
+ MOCK_CONST_METHOD1(GetNSGradient, NSGradient*(int));
+};
+
+// Allows us to inject our fake controller below.
+@interface BookmarkBarToolbarView (TestingAPI)
+-(void)setController:(id<BookmarkBarToolbarViewController>)controller;
+@end
+
+@implementation BookmarkBarToolbarView (TestingAPI)
+-(void)setController:(id<BookmarkBarToolbarViewController>)controller {
+ controller_ = controller;
+}
+@end
+
+// Allows us to control which way the view is rendered.
+@interface DrawDetachedBarFakeController :
+ NSObject<BookmarkBarState, BookmarkBarToolbarViewController> {
+ @private
+ int currentTabContentsHeight_;
+ ThemeProvider* themeProvider_;
+ bookmarks::VisualState visualState_;
+}
+@property (nonatomic, assign) int currentTabContentsHeight;
+@property (nonatomic, assign) ThemeProvider* themeProvider;
+@property (nonatomic, assign) bookmarks::VisualState visualState;
+
+// |BookmarkBarState| protocol:
+- (BOOL)isVisible;
+- (BOOL)isAnimationRunning;
+- (BOOL)isInState:(bookmarks::VisualState)state;
+- (BOOL)isAnimatingToState:(bookmarks::VisualState)state;
+- (BOOL)isAnimatingFromState:(bookmarks::VisualState)state;
+- (BOOL)isAnimatingFromState:(bookmarks::VisualState)fromState
+ toState:(bookmarks::VisualState)toState;
+- (BOOL)isAnimatingBetweenState:(bookmarks::VisualState)fromState
+ andState:(bookmarks::VisualState)toState;
+- (CGFloat)detachedMorphProgress;
+@end
+
+@implementation DrawDetachedBarFakeController
+@synthesize currentTabContentsHeight = currentTabContentsHeight_;
+@synthesize themeProvider = themeProvider_;
+@synthesize visualState = visualState_;
+
+- (id)init {
+ if ((self = [super init])) {
+ [self setVisualState:bookmarks::kHiddenState];
+ }
+ return self;
+}
+
+- (BOOL)isVisible { return YES; }
+- (BOOL)isAnimationRunning { return NO; }
+- (BOOL)isInState:(bookmarks::VisualState)state
+ { return ([self visualState] == state) ? YES : NO; }
+- (BOOL)isAnimatingToState:(bookmarks::VisualState)state { return NO; }
+- (BOOL)isAnimatingFromState:(bookmarks::VisualState)state { return NO; }
+- (BOOL)isAnimatingFromState:(bookmarks::VisualState)fromState
+ toState:(bookmarks::VisualState)toState { return NO; }
+- (BOOL)isAnimatingBetweenState:(bookmarks::VisualState)fromState
+ andState:(bookmarks::VisualState)toState { return NO; }
+- (CGFloat)detachedMorphProgress { return 1; }
+@end
+
+class BookmarkBarToolbarViewTest : public CocoaTest {
+ public:
+ BookmarkBarToolbarViewTest() {
+ controller_.reset([[DrawDetachedBarFakeController alloc] init]);
+ NSRect frame = NSMakeRect(0, 0, 400, 40);
+ scoped_nsobject<BookmarkBarToolbarView> view(
+ [[BookmarkBarToolbarView alloc] initWithFrame:frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ [view_ setController:controller_.get()];
+ }
+
+ scoped_nsobject<DrawDetachedBarFakeController> controller_;
+ BookmarkBarToolbarView* view_;
+};
+
+TEST_VIEW(BookmarkBarToolbarViewTest, view_)
+
+// Test drawing (part 1), mostly to ensure nothing leaks or crashes.
+TEST_F(BookmarkBarToolbarViewTest, DisplayAsNormalBar) {
+ [controller_.get() setVisualState:bookmarks::kShowingState];
+ [view_ display];
+}
+
+// Test drawing (part 2), mostly to ensure nothing leaks or crashes.
+TEST_F(BookmarkBarToolbarViewTest, DisplayAsDetachedBarWithNoImage) {
+ [controller_.get() setVisualState:bookmarks::kDetachedState];
+
+ // Tests where we don't have a background image, only a color.
+ MockThemeProvider provider;
+ EXPECT_CALL(provider, GetColor(BrowserThemeProvider::COLOR_NTP_BACKGROUND))
+ .WillRepeatedly(Return(SK_ColorWHITE));
+ EXPECT_CALL(provider, HasCustomImage(IDR_THEME_NTP_BACKGROUND))
+ .WillRepeatedly(Return(false));
+ [controller_.get() setThemeProvider:&provider];
+
+ [view_ display];
+}
+
+// Actions used in DisplayAsDetachedBarWithBgImage.
+ACTION(SetBackgroundTiling) {
+ *arg1 = BrowserThemeProvider::NO_REPEAT;
+ return true;
+}
+
+ACTION(SetAlignLeft) {
+ *arg1 = BrowserThemeProvider::ALIGN_LEFT;
+ return true;
+}
+
+// Test drawing (part 3), mostly to ensure nothing leaks or crashes.
+TEST_F(BookmarkBarToolbarViewTest, DisplayAsDetachedBarWithBgImage) {
+ [controller_.get() setVisualState:bookmarks::kDetachedState];
+
+ // Tests where we have a background image, with positioning information.
+ MockThemeProvider provider;
+
+ // Advertise having an image.
+ EXPECT_CALL(provider, GetColor(BrowserThemeProvider::COLOR_NTP_BACKGROUND))
+ .WillRepeatedly(Return(SK_ColorRED));
+ EXPECT_CALL(provider, HasCustomImage(IDR_THEME_NTP_BACKGROUND))
+ .WillRepeatedly(Return(true));
+
+ // Return the correct tiling/alignment information.
+ EXPECT_CALL(provider,
+ GetDisplayProperty(BrowserThemeProvider::NTP_BACKGROUND_TILING, _))
+ .WillRepeatedly(SetBackgroundTiling());
+ EXPECT_CALL(provider,
+ GetDisplayProperty(BrowserThemeProvider::NTP_BACKGROUND_ALIGNMENT, _))
+ .WillRepeatedly(SetAlignLeft());
+
+ // Create a dummy bitmap full of not-red to blit with.
+ SkBitmap fake_bg;
+ fake_bg.setConfig(SkBitmap::kARGB_8888_Config, 800, 800);
+ fake_bg.allocPixels();
+ fake_bg.eraseColor(SK_ColorGREEN);
+ EXPECT_CALL(provider, GetBitmapNamed(IDR_THEME_NTP_BACKGROUND))
+ .WillRepeatedly(Return(&fake_bg));
+
+ [controller_.get() setThemeProvider:&provider];
+ [controller_.get() setCurrentTabContentsHeight:200];
+
+ [view_ display];
+}
+
+// TODO(viettrungluu): write more unit tests, especially after my refactoring.
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.h
new file mode 100644
index 0000000..d0221b2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.h
@@ -0,0 +1,57 @@
+// 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_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_UNITTEST_HELPER_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_UNITTEST_HELPER_H_
+#pragma once
+
+#import <Foundation/Foundation.h>
+
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
+
+@interface BookmarkBarController (BookmarkBarUnitTestHelper)
+
+// Return the bookmark button from this bar controller with the given
+// |title|, otherwise nil. This does not recurse into folders.
+- (BookmarkButton*)buttonWithTitleEqualTo:(NSString*)title;
+
+@end
+
+
+@interface BookmarkBarFolderController (BookmarkBarUnitTestHelper)
+
+// Return the bookmark button from this folder controller with the given
+// |title|, otherwise nil. This does not recurse into subfolders.
+- (BookmarkButton*)buttonWithTitleEqualTo:(NSString*)title;
+
+@end
+
+
+@interface BookmarkButton (BookmarkBarUnitTestHelper)
+
+// Return the center of the button in the base coordinate system of the
+// containing window. Useful for simulating mouse clicks or drags.
+- (NSPoint)center;
+
+// Return the top of the button in the base coordinate system of the
+// containing window.
+- (NSPoint)top;
+
+// Return the bottom of the button in the base coordinate system of the
+// containing window.
+- (NSPoint)bottom;
+
+// Return the center-left point of the button in the base coordinate system
+// of the containing window.
+- (NSPoint)left;
+
+// Return the center-right point of the button in the base coordinate system
+// of the containing window.
+- (NSPoint)right;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_UNITTEST_HELPER_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.mm
new file mode 100644
index 0000000..7cddec4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.mm
@@ -0,0 +1,81 @@
+// 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/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.h"
+
+@interface NSArray (BookmarkBarUnitTestHelper)
+
+// A helper function for scanning an array of buttons looking for the
+// button with the given |title|.
+- (BookmarkButton*)buttonWithTitleEqualTo:(NSString*)title;
+
+@end
+
+
+@implementation NSArray (BookmarkBarUnitTestHelper)
+
+- (BookmarkButton*)buttonWithTitleEqualTo:(NSString*)title {
+ for (BookmarkButton* button in self) {
+ if ([[button title] isEqualToString:title])
+ return button;
+ }
+ return nil;
+}
+
+@end
+
+@implementation BookmarkBarController (BookmarkBarUnitTestHelper)
+
+- (BookmarkButton*)buttonWithTitleEqualTo:(NSString*)title {
+ return [[self buttons] buttonWithTitleEqualTo:title];
+}
+
+@end
+
+@implementation BookmarkBarFolderController(BookmarkBarUnitTestHelper)
+
+- (BookmarkButton*)buttonWithTitleEqualTo:(NSString*)title {
+ return [[self buttons] buttonWithTitleEqualTo:title];
+}
+
+@end
+
+@implementation BookmarkButton(BookmarkBarUnitTestHelper)
+
+- (NSPoint)center {
+ NSRect frame = [self frame];
+ NSPoint center = NSMakePoint(NSMidX(frame), NSMidY(frame));
+ center = [[self superview] convertPoint:center toView:nil];
+ return center;
+}
+
+- (NSPoint)top {
+ NSRect frame = [self frame];
+ NSPoint top = NSMakePoint(NSMidX(frame), NSMaxY(frame));
+ top = [[self superview] convertPoint:top toView:nil];
+ return top;
+}
+
+- (NSPoint)bottom {
+ NSRect frame = [self frame];
+ NSPoint bottom = NSMakePoint(NSMidX(frame), NSMinY(frame));
+ bottom = [[self superview] convertPoint:bottom toView:nil];
+ return bottom;
+}
+
+- (NSPoint)left {
+ NSRect frame = [self frame];
+ NSPoint left = NSMakePoint(NSMinX(frame), NSMidY(frame));
+ left = [[self superview] convertPoint:left toView:nil];
+ return left;
+}
+
+- (NSPoint)right {
+ NSRect frame = [self frame];
+ NSPoint right = NSMakePoint(NSMaxX(frame), NSMidY(frame));
+ right = [[self superview] convertPoint:right toView:nil];
+ return right;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h
new file mode 100644
index 0000000..abcbdf0
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h
@@ -0,0 +1,41 @@
+// 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.
+//
+// A simple custom NSView for the bookmark bar used to prevent clicking and
+// dragging from moving the browser window.
+
+#ifndef CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/background_gradient_view.h"
+
+@class BookmarkBarController;
+
+@interface BookmarkBarView : BackgroundGradientView {
+ @private
+ BOOL dropIndicatorShown_;
+ CGFloat dropIndicatorPosition_; // x position
+
+ IBOutlet BookmarkBarController* controller_;
+ IBOutlet NSTextField* noItemTextfield_;
+ IBOutlet NSButton* importBookmarksButton_;
+ NSView* noItemContainer_;
+}
+- (NSTextField*)noItemTextfield;
+- (NSButton*)importBookmarksButton;
+- (BookmarkBarController*)controller;
+
+@property (nonatomic, assign) IBOutlet NSView* noItemContainer;
+@end
+
+@interface BookmarkBarView() // TestingOrInternalAPI
+@property (nonatomic, readonly) BOOL dropIndicatorShown;
+@property (nonatomic, readonly) CGFloat dropIndicatorPosition;
+- (void)setController:(id)controller;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.mm
new file mode 100644
index 0000000..5083367
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.mm
@@ -0,0 +1,259 @@
+// 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/ui/cocoa/bookmarks/bookmark_bar_view.h"
+
+#include "chrome/browser/bookmarks/bookmark_pasteboard_helper_mac.h"
+#include "chrome/browser/metrics/user_metrics.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+#import "chrome/browser/themes/browser_theme_provider.h"
+#import "third_party/mozilla/NSPasteboard+Utils.h"
+
+@interface BookmarkBarView (Private)
+- (void)themeDidChangeNotification:(NSNotification*)aNotification;
+- (void)updateTheme:(ThemeProvider*)themeProvider;
+@end
+
+@implementation BookmarkBarView
+
+@synthesize dropIndicatorShown = dropIndicatorShown_;
+@synthesize dropIndicatorPosition = dropIndicatorPosition_;
+@synthesize noItemContainer = noItemContainer_;
+
+- (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 {
+ NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
+ [defaultCenter addObserver:self
+ selector:@selector(themeDidChangeNotification:)
+ name:kBrowserThemeDidChangeNotification
+ object:nil];
+
+ DCHECK(controller_) << "Expected this to be hooked up via Interface Builder";
+ NSArray* types = [NSArray arrayWithObjects:
+ NSStringPboardType,
+ NSHTMLPboardType,
+ NSURLPboardType,
+ kBookmarkButtonDragType,
+ kBookmarkDictionaryListPboardType,
+ nil];
+ [self registerForDraggedTypes:types];
+}
+
+// We need the theme to color the bookmark buttons properly. But our
+// controller desn't have access to it until it's placed in the view
+// hierarchy. This is the spot where we close the loop.
+- (void)viewWillMoveToWindow:(NSWindow*)window {
+ ThemeProvider* themeProvider = [window themeProvider];
+ [self updateTheme:themeProvider];
+ [controller_ updateTheme:themeProvider];
+}
+
+- (void)viewDidMoveToWindow {
+ [controller_ viewDidMoveToWindow];
+}
+
+// Called after the current theme has changed.
+- (void)themeDidChangeNotification:(NSNotification*)aNotification {
+ ThemeProvider* themeProvider =
+ static_cast<ThemeProvider*>([[aNotification object] pointerValue]);
+ [self updateTheme:themeProvider];
+}
+
+// Adapt appearance to the current theme. Called after theme changes and before
+// this is shown for the first time.
+- (void)updateTheme:(ThemeProvider*)themeProvider {
+ if (!themeProvider)
+ return;
+
+ NSColor* color =
+ themeProvider->GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT,
+ true);
+ [noItemTextfield_ setTextColor:color];
+}
+
+// Mouse down events on the bookmark bar should not allow dragging the parent
+// window around.
+- (BOOL)mouseDownCanMoveWindow {
+ return NO;
+}
+
+-(NSTextField*)noItemTextfield {
+ return noItemTextfield_;
+}
+
+-(NSButton*)importBookmarksButton {
+ return importBookmarksButton_;
+}
+
+- (BookmarkBarController*)controller {
+ return controller_;
+}
+
+-(void)drawRect:(NSRect)dirtyRect {
+ [super drawRect:dirtyRect];
+
+ // Draw the bookmark-button-dragging drop indicator if necessary.
+ if (dropIndicatorShown_) {
+ const CGFloat kBarWidth = 1;
+ const CGFloat kBarHalfWidth = kBarWidth / 2.0;
+ const CGFloat kBarVertPad = 4;
+ const CGFloat kBarOpacity = 0.85;
+
+ // Prevent the indicator from being clipped on the left.
+ CGFloat xLeft = MAX(dropIndicatorPosition_ - kBarHalfWidth, 0);
+
+ NSRect uglyBlackBar =
+ NSMakeRect(xLeft, kBarVertPad,
+ kBarWidth, NSHeight([self bounds]) - 2 * kBarVertPad);
+ NSColor* uglyBlackBarColor = [[self window] themeProvider]->
+ GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT, true);
+ [[uglyBlackBarColor colorWithAlphaComponent:kBarOpacity] setFill];
+ [[NSBezierPath bezierPathWithRect:uglyBlackBar] fill];
+ }
+}
+
+// Shim function to assist in unit testing.
+- (BOOL)dragClipboardContainsBookmarks {
+ return bookmark_pasteboard_helper_mac::DragClipboardContainsBookmarks();
+}
+
+// NSDraggingDestination methods
+
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info {
+ if ([[info draggingPasteboard] dataForType:kBookmarkButtonDragType] ||
+ [self dragClipboardContainsBookmarks] ||
+ [[info draggingPasteboard] containsURLData]) {
+ // 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_ indicatorPosForDragToPoint:[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 [info draggingSource] ? NSDragOperationMove : NSDragOperationCopy;
+ }
+ return NSDragOperationNone;
+}
+
+- (void)draggingExited:(id<NSDraggingInfo>)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 {
+ // For now, we just call |-draggingExited:|.
+ [self draggingExited:info];
+}
+
+- (BOOL)wantsPeriodicDraggingUpdates {
+ // TODO(port): 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.
+ 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 controller_ to perform animations.
+ return [self draggingEntered:info];
+}
+
+- (BOOL)prepareForDragOperation:(id<NSDraggingInfo>)info {
+ return YES;
+}
+
+// Implement NSDraggingDestination protocol method
+// performDragOperation: for URLs.
+- (BOOL)performDragOperationForURL:(id<NSDraggingInfo>)info {
+ NSPasteboard* pboard = [info draggingPasteboard];
+ DCHECK([pboard containsURLData]);
+
+ NSArray* urls = nil;
+ NSArray* titles = nil;
+ [pboard getURLs:&urls andTitles:&titles convertingFilenames:YES];
+
+ return [controller_ addURLs:urls
+ withTitles:titles
+ at:[info draggingLocation]];
+}
+
+// Implement NSDraggingDestination protocol method
+// performDragOperation: for bookmark buttons.
+- (BOOL)performDragOperationForBookmarkButton:(id<NSDraggingInfo>)info {
+ BOOL rtn = 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)];
+ BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove);
+ rtn = [controller_ dragButton:button
+ to:[info draggingLocation]
+ copy:copy];
+ UserMetrics::RecordAction(UserMetricsAction("BookmarkBar_DragEnd"));
+ }
+ return rtn;
+}
+
+- (BOOL)performDragOperation:(id<NSDraggingInfo>)info {
+ if ([controller_ dragBookmarkData:info])
+ return YES;
+ NSPasteboard* pboard = [info draggingPasteboard];
+ if ([pboard dataForType:kBookmarkButtonDragType]) {
+ if ([self performDragOperationForBookmarkButton:info])
+ return YES;
+ // Fall through....
+ }
+ if ([pboard containsURLData]) {
+ if ([self performDragOperationForURL:info])
+ return YES;
+ }
+ return NO;
+}
+
+- (void)setController:(id)controller {
+ controller_ = controller;
+}
+
+- (ViewID)viewID {
+ return VIEW_ID_BOOKMARK_BAR;
+}
+
+@end // @implementation BookmarkBarView
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view_unittest.mm
new file mode 100644
index 0000000..c847a61
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view_unittest.mm
@@ -0,0 +1,215 @@
+// 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/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/url_drop_target.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+#import "third_party/mozilla/NSPasteboard+Utils.h"
+
+namespace {
+ const CGFloat kFakeIndicatorPos = 7.0;
+};
+
+// Fake DraggingInfo, fake BookmarkBarController, fake NSPasteboard...
+@interface FakeBookmarkDraggingInfo : NSObject {
+ @public
+ BOOL dragButtonToPong_;
+ BOOL dragURLsPong_;
+ BOOL dragBookmarkDataPong_;
+ BOOL dropIndicatorShown_;
+ BOOL draggingEnteredCalled_;
+ // Only mock one type of drag data at a time.
+ NSString* dragDataType_;
+}
+@property (nonatomic) BOOL dropIndicatorShown;
+@property (nonatomic) BOOL draggingEnteredCalled;
+@property (nonatomic, copy) NSString* dragDataType;
+@end
+
+@implementation FakeBookmarkDraggingInfo
+
+@synthesize dropIndicatorShown = dropIndicatorShown_;
+@synthesize draggingEnteredCalled = draggingEnteredCalled_;
+@synthesize dragDataType = dragDataType_;
+
+- (id)init {
+ if ((self = [super init])) {
+ dropIndicatorShown_ = YES;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [dragDataType_ release];
+ [super dealloc];
+}
+
+- (void)reset {
+ [dragDataType_ release];
+ dragDataType_ = nil;
+ dragButtonToPong_ = NO;
+ dragURLsPong_ = NO;
+ dragBookmarkDataPong_ = NO;
+ dropIndicatorShown_ = YES;
+ draggingEnteredCalled_ = NO;
+}
+
+// NSDragInfo mocking functions.
+
+- (id)draggingPasteboard {
+ return self;
+}
+
+// So we can look local.
+- (id)draggingSource {
+ return self;
+}
+
+- (NSDragOperation)draggingSourceOperationMask {
+ return NSDragOperationCopy | NSDragOperationMove;
+}
+
+- (NSPoint)draggingLocation {
+ return NSMakePoint(10, 10);
+}
+
+// NSPasteboard mocking functions.
+
+- (BOOL)containsURLData {
+ NSArray* urlTypes = [URLDropTargetHandler handledDragTypes];
+ if (dragDataType_)
+ return [urlTypes containsObject:dragDataType_];
+ return NO;
+}
+
+- (NSData*)dataForType:(NSString*)type {
+ if (dragDataType_ && [dragDataType_ isEqualToString:type])
+ return [NSData data]; // Return something, anything.
+ return nil;
+}
+
+// Fake a controller for callback ponging
+
+- (BOOL)dragButton:(BookmarkButton*)button to:(NSPoint)point copy:(BOOL)copy {
+ dragButtonToPong_ = YES;
+ return YES;
+}
+
+- (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point {
+ dragURLsPong_ = YES;
+ return YES;
+}
+
+- (void)getURLs:(NSArray**)outUrls
+ andTitles:(NSArray**)outTitles
+ convertingFilenames:(BOOL)convertFilenames {
+}
+
+- (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info {
+ dragBookmarkDataPong_ = YES;
+ return NO;
+}
+
+// Confirm the pongs.
+
+- (BOOL)dragButtonToPong {
+ return dragButtonToPong_;
+}
+
+- (BOOL)dragURLsPong {
+ return dragURLsPong_;
+}
+
+- (BOOL)dragBookmarkDataPong {
+ return dragBookmarkDataPong_;
+}
+
+- (CGFloat)indicatorPosForDragToPoint:(NSPoint)point {
+ return kFakeIndicatorPos;
+}
+
+- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point {
+ return dropIndicatorShown_;
+}
+
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info {
+ draggingEnteredCalled_ = YES;
+ return NSDragOperationNone;
+}
+
+@end
+
+namespace {
+
+class BookmarkBarViewTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ view_.reset([[BookmarkBarView alloc] init]);
+ }
+
+ scoped_nsobject<BookmarkBarView> view_;
+};
+
+TEST_F(BookmarkBarViewTest, CanDragWindow) {
+ EXPECT_FALSE([view_ mouseDownCanMoveWindow]);
+}
+
+TEST_F(BookmarkBarViewTest, BookmarkButtonDragAndDrop) {
+ scoped_nsobject<FakeBookmarkDraggingInfo>
+ info([[FakeBookmarkDraggingInfo alloc] init]);
+ [view_ setController:info.get()];
+ [info reset];
+
+ [info setDragDataType:kBookmarkButtonDragType];
+ EXPECT_EQ([view_ draggingEntered:(id)info.get()], NSDragOperationMove);
+ EXPECT_TRUE([view_ performDragOperation:(id)info.get()]);
+ EXPECT_TRUE([info dragButtonToPong]);
+ EXPECT_FALSE([info dragURLsPong]);
+ EXPECT_TRUE([info dragBookmarkDataPong]);
+}
+
+TEST_F(BookmarkBarViewTest, URLDragAndDrop) {
+ scoped_nsobject<FakeBookmarkDraggingInfo>
+ info([[FakeBookmarkDraggingInfo alloc] init]);
+ [view_ setController:info.get()];
+ [info reset];
+
+ NSArray* dragTypes = [URLDropTargetHandler handledDragTypes];
+ for (NSString* type in dragTypes) {
+ [info setDragDataType:type];
+ EXPECT_EQ([view_ draggingEntered:(id)info.get()], NSDragOperationMove);
+ EXPECT_TRUE([view_ performDragOperation:(id)info.get()]);
+ EXPECT_FALSE([info dragButtonToPong]);
+ EXPECT_TRUE([info dragURLsPong]);
+ EXPECT_TRUE([info dragBookmarkDataPong]);
+ [info reset];
+ }
+}
+
+TEST_F(BookmarkBarViewTest, BookmarkButtonDropIndicator) {
+ scoped_nsobject<FakeBookmarkDraggingInfo>
+ info([[FakeBookmarkDraggingInfo alloc] init]);
+ [view_ setController:info.get()];
+
+ [info reset];
+ [info setDragDataType:kBookmarkButtonDragType];
+ EXPECT_FALSE([info draggingEnteredCalled]);
+ EXPECT_EQ([view_ draggingEntered:(id)info.get()], NSDragOperationMove);
+ EXPECT_TRUE([info draggingEnteredCalled]); // Ensure controller pinged.
+ 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/ui/cocoa/bookmarks/bookmark_bubble_controller.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.h
new file mode 100644
index 0000000..fc2840d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.h
@@ -0,0 +1,81 @@
+// 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 "base/cocoa_protocols_mac.h"
+#include "base/scoped_ptr.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa.h"
+
+class BookmarkBubbleNotificationBridge;
+class BookmarkModel;
+class BookmarkNode;
+@class BookmarkBubbleController;
+@class InfoBubbleView;
+
+
+// Controller for the bookmark bubble. The bookmark bubble is a
+// bubble that pops up when clicking on the STAR next to the URL to
+// add or remove it as a bookmark. This bubble allows for editing of
+// the bookmark in various ways (name, folder, etc.)
+@interface BookmarkBubbleController : NSWindowController<NSWindowDelegate> {
+ @private
+ NSWindow* parentWindow_; // weak
+
+ // Both weak; owned by the current browser's profile
+ BookmarkModel* model_; // weak
+ const BookmarkNode* node_; // weak
+
+ // The bookmark node whose button we asked to pulse.
+ const BookmarkNode* pulsingBookmarkNode_; // weak
+
+ BOOL alreadyBookmarked_;
+
+ // Ping me when the bookmark model changes out from under us.
+ scoped_ptr<BookmarkModelObserverForCocoa> bookmark_observer_;
+
+ // Ping me when other Chrome things change out from under us.
+ scoped_ptr<BookmarkBubbleNotificationBridge> chrome_observer_;
+
+ IBOutlet NSTextField* bigTitle_; // "Bookmark" or "Bookmark Added!"
+ IBOutlet NSTextField* nameTextField_;
+ IBOutlet NSPopUpButton* folderPopUpButton_;
+ IBOutlet InfoBubbleView* bubble_; // to set arrow position
+}
+
+@property (readonly, nonatomic) const BookmarkNode* node;
+
+// |node| is the bookmark node we edit in this bubble.
+// |alreadyBookmarked| tells us if the node was bookmarked before the
+// user clicked on the star. (if NO, this is a brand new bookmark).
+// The owner of this object is responsible for showing the bubble if
+// it desires it to be visible on the screen. It is not shown by the
+// init routine. Closing of the window happens implicitly on dealloc.
+- (id)initWithParentWindow:(NSWindow*)parentWindow
+ model:(BookmarkModel*)model
+ node:(const BookmarkNode*)node
+ alreadyBookmarked:(BOOL)alreadyBookmarked;
+
+// Actions for buttons in the dialog.
+- (IBAction)ok:(id)sender;
+- (IBAction)remove:(id)sender;
+- (IBAction)cancel:(id)sender;
+
+// These actions send a -editBookmarkNode: action up the responder chain.
+- (IBAction)edit:(id)sender;
+- (IBAction)folderChanged:(id)sender;
+
+@end
+
+
+// Exposed only for unit testing.
+@interface BookmarkBubbleController(ExposedForUnitTesting)
+- (void)addFolderNodes:(const BookmarkNode*)parent
+ toPopUpButton:(NSPopUpButton*)button
+ indentation:(int)indentation;
+- (void)setTitle:(NSString*)title parentFolder:(const BookmarkNode*)parent;
+- (void)setParentFolderSelection:(const BookmarkNode*)parent;
++ (NSString*)chooseAnotherFolderString;
+- (NSPopUpButton*)folderPopUpButton;
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.mm
new file mode 100644
index 0000000..ae66081
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.mm
@@ -0,0 +1,428 @@
+// 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/ui/cocoa/bookmarks/bookmark_bubble_controller.h"
+
+#include "app/l10n_util_mac.h"
+#include "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h" // TODO(viettrungluu): remove
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/metrics/user_metrics.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/info_bubble_view.h"
+#include "chrome/common/notification_observer.h"
+#include "chrome/common/notification_registrar.h"
+#include "chrome/common/notification_service.h"
+#include "grit/generated_resources.h"
+
+
+// Simple class to watch for tab creation/destruction and close the bubble.
+// Bridge between Chrome-style notifications and ObjC-style notifications.
+class BookmarkBubbleNotificationBridge : public NotificationObserver {
+ public:
+ BookmarkBubbleNotificationBridge(BookmarkBubbleController* controller,
+ SEL selector);
+ virtual ~BookmarkBubbleNotificationBridge() {}
+ void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+ private:
+ NotificationRegistrar registrar_;
+ BookmarkBubbleController* controller_; // weak; owns us.
+ SEL selector_; // SEL sent to controller_ on notification.
+};
+
+BookmarkBubbleNotificationBridge::BookmarkBubbleNotificationBridge(
+ BookmarkBubbleController* controller, SEL selector)
+ : controller_(controller), selector_(selector) {
+ // registrar_ will automatically RemoveAll() when destroyed so we
+ // don't need to do so explicitly.
+ registrar_.Add(this, NotificationType::TAB_CONTENTS_CONNECTED,
+ NotificationService::AllSources());
+ registrar_.Add(this, NotificationType::TAB_CLOSED,
+ NotificationService::AllSources());
+}
+
+// At this time all notifications instigate the same behavior (go
+// away) so we don't bother checking which notification came in.
+void BookmarkBubbleNotificationBridge::Observe(
+ NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ [controller_ performSelector:selector_ withObject:controller_];
+}
+
+
+// An object to represent the ChooseAnotherFolder item in the pop up.
+@interface ChooseAnotherFolder : NSObject
+@end
+
+@implementation ChooseAnotherFolder
+@end
+
+@interface BookmarkBubbleController (PrivateAPI)
+- (void)updateBookmarkNode;
+- (void)fillInFolderList;
+- (void)parentWindowWillClose:(NSNotification*)notification;
+@end
+
+@implementation BookmarkBubbleController
+
+@synthesize node = node_;
+
++ (id)chooseAnotherFolderObject {
+ // Singleton object to act as a representedObject for the "choose another
+ // folder" item in the pop up.
+ static ChooseAnotherFolder* object = nil;
+ if (!object) {
+ object = [[ChooseAnotherFolder alloc] init];
+ }
+ return object;
+}
+
+- (id)initWithParentWindow:(NSWindow*)parentWindow
+ model:(BookmarkModel*)model
+ node:(const BookmarkNode*)node
+ alreadyBookmarked:(BOOL)alreadyBookmarked {
+ NSString* nibPath =
+ [mac_util::MainAppBundle() pathForResource:@"BookmarkBubble"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
+ parentWindow_ = parentWindow;
+ model_ = model;
+ node_ = node;
+ alreadyBookmarked_ = alreadyBookmarked;
+
+ // Watch to see if the parent window closes, and if so, close this one.
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
+ selector:@selector(parentWindowWillClose:)
+ name:NSWindowWillCloseNotification
+ object:parentWindow_];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+// If this is a new bookmark somewhere visible (e.g. on the bookmark
+// bar), pulse it. Else, call ourself recursively with our parent
+// until we find something visible to pulse.
+- (void)startPulsingBookmarkButton:(const BookmarkNode*)node {
+ while (node) {
+ if ((node->GetParent() == model_->GetBookmarkBarNode()) ||
+ (node == model_->other_node())) {
+ pulsingBookmarkNode_ = node;
+ NSValue *value = [NSValue valueWithPointer:node];
+ NSDictionary *dict = [NSDictionary
+ dictionaryWithObjectsAndKeys:value,
+ bookmark_button::kBookmarkKey,
+ [NSNumber numberWithBool:YES],
+ bookmark_button::kBookmarkPulseFlagKey,
+ nil];
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:bookmark_button::kPulseBookmarkButtonNotification
+ object:self
+ userInfo:dict];
+ return;
+ }
+ node = node->GetParent();
+ }
+}
+
+- (void)stopPulsingBookmarkButton {
+ if (!pulsingBookmarkNode_)
+ return;
+ NSValue *value = [NSValue valueWithPointer:pulsingBookmarkNode_];
+ pulsingBookmarkNode_ = NULL;
+ NSDictionary *dict = [NSDictionary
+ dictionaryWithObjectsAndKeys:value,
+ bookmark_button::kBookmarkKey,
+ [NSNumber numberWithBool:NO],
+ bookmark_button::kBookmarkPulseFlagKey,
+ nil];
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:bookmark_button::kPulseBookmarkButtonNotification
+ object:self
+ userInfo:dict];
+}
+
+// Close the bookmark bubble without changing anything. Unlike a
+// typical dialog's OK/Cancel, where Cancel is "do nothing", all
+// buttons on the bubble have the capacity to change the bookmark
+// model. This is an IBOutlet-looking entry point to remove the
+// dialog without touching the model.
+- (void)dismissWithoutEditing:(id)sender {
+ [self close];
+}
+
+- (void)parentWindowWillClose:(NSNotification*)notification {
+ [self close];
+}
+
+- (void)windowWillClose:(NSNotification*)notification {
+ // We caught a close so we don't need to watch for the parent closing.
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ bookmark_observer_.reset(NULL);
+ chrome_observer_.reset(NULL);
+ [self stopPulsingBookmarkButton];
+ [self autorelease];
+}
+
+// We want this to be a child of a browser window. addChildWindow:
+// (called from this function) will bring the window on-screen;
+// unfortunately, [NSWindowController showWindow:] will also bring it
+// on-screen (but will cause unexpected changes to the window's
+// position). We cannot have an addChildWindow: and a subsequent
+// showWindow:. Thus, we have our own version.
+- (void)showWindow:(id)sender {
+ BrowserWindowController* bwc =
+ [BrowserWindowController browserWindowControllerForWindow:parentWindow_];
+ [bwc lockBarVisibilityForOwner:self withAnimation:NO delay:NO];
+ NSWindow* window = [self window]; // completes nib load
+ [bubble_ setArrowLocation:info_bubble::kTopRight];
+ // Insure decent positioning even in the absence of a browser controller,
+ // which will occur for some unit tests.
+ NSPoint arrowtip = bwc ? [bwc bookmarkBubblePoint] :
+ NSMakePoint([window frame].size.width, [window frame].size.height);
+ NSPoint origin = [parentWindow_ convertBaseToScreen:arrowtip];
+ NSPoint bubbleArrowtip = [bubble_ arrowTip];
+ bubbleArrowtip = [bubble_ convertPoint:bubbleArrowtip toView:nil];
+ origin.y -= bubbleArrowtip.y;
+ origin.x -= bubbleArrowtip.x;
+ [window setFrameOrigin:origin];
+ [parentWindow_ addChildWindow:window ordered:NSWindowAbove];
+ // Default is IDS_BOOMARK_BUBBLE_PAGE_BOOKMARK; "Bookmark".
+ // If adding for the 1st time the string becomes "Bookmark Added!"
+ if (!alreadyBookmarked_) {
+ NSString* title =
+ l10n_util::GetNSString(IDS_BOOMARK_BUBBLE_PAGE_BOOKMARKED);
+ [bigTitle_ setStringValue:title];
+ }
+
+ [self fillInFolderList];
+
+ // Ping me when things change out from under us. Unlike a normal
+ // dialog, the bookmark bubble's cancel: means "don't add this as a
+ // bookmark", not "cancel editing". We must take extra care to not
+ // touch the bookmark in this selector.
+ bookmark_observer_.reset(new BookmarkModelObserverForCocoa(
+ node_, model_,
+ self,
+ @selector(dismissWithoutEditing:)));
+ chrome_observer_.reset(new BookmarkBubbleNotificationBridge(
+ self, @selector(dismissWithoutEditing:)));
+
+ // Pulse something interesting on the bookmark bar.
+ [self startPulsingBookmarkButton:node_];
+
+ [window makeKeyAndOrderFront:self];
+}
+
+- (void)close {
+ [[BrowserWindowController browserWindowControllerForWindow:parentWindow_]
+ releaseBarVisibilityForOwner:self withAnimation:YES delay:NO];
+ [parentWindow_ removeChildWindow:[self window]];
+
+ // If you quit while the bubble is open, sometimes we get a
+ // DidResignKey before we get our parent's WindowWillClose and
+ // sometimes not. We protect against a multiple close (or reference
+ // to parentWindow_ at a bad time) by clearing it out once we're
+ // done, and by removing ourself from future notifications.
+ [[NSNotificationCenter defaultCenter]
+ removeObserver:self
+ name:NSWindowWillCloseNotification
+ object:parentWindow_];
+ parentWindow_ = nil;
+
+ [super close];
+}
+
+// Shows the bookmark editor sheet for more advanced editing.
+- (void)showEditor {
+ [self ok:self];
+ // Send the action up through the responder chain.
+ [NSApp sendAction:@selector(editBookmarkNode:) to:nil from:self];
+}
+
+- (IBAction)edit:(id)sender {
+ UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Edit"),
+ model_->profile());
+ [self showEditor];
+}
+
+- (IBAction)ok:(id)sender {
+ [self stopPulsingBookmarkButton]; // before parent changes
+ [self updateBookmarkNode];
+ [self close];
+}
+
+// By implementing this, ESC causes the window to go away. If clicking the
+// star was what prompted this bubble to appear (i.e., not already bookmarked),
+// remove the bookmark.
+- (IBAction)cancel:(id)sender {
+ if (!alreadyBookmarked_) {
+ // |-remove:| calls |-close| so don't do it.
+ [self remove:sender];
+ } else {
+ [self ok:sender];
+ }
+}
+
+- (IBAction)remove:(id)sender {
+ [self stopPulsingBookmarkButton];
+ // TODO(viettrungluu): get rid of conversion and utf_string_conversions.h.
+ model_->SetURLStarred(node_->GetURL(), node_->GetTitle(), false);
+ UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Unstar"),
+ model_->profile());
+ node_ = NULL; // no longer valid
+ [self ok:sender];
+}
+
+// The controller is the target of the pop up button box action so it can
+// handle when "choose another folder" was picked.
+- (IBAction)folderChanged:(id)sender {
+ DCHECK([sender isEqual:folderPopUpButton_]);
+ // It is possible that due to model change our parent window has been closed
+ // but the popup is still showing and able to notify the controller of a
+ // folder change. We ignore the sender in this case.
+ if (!parentWindow_)
+ return;
+ NSMenuItem* selected = [folderPopUpButton_ selectedItem];
+ ChooseAnotherFolder* chooseItem = [[self class] chooseAnotherFolderObject];
+ if ([[selected representedObject] isEqual:chooseItem]) {
+ UserMetrics::RecordAction(
+ UserMetricsAction("BookmarkBubble_EditFromCombobox"),
+ model_->profile());
+ [self showEditor];
+ }
+}
+
+// The controller is the delegate of the window so it receives did resign key
+// notifications. When key is resigned mirror Windows behavior and close the
+// window.
+- (void)windowDidResignKey:(NSNotification*)notification {
+ NSWindow* window = [self window];
+ DCHECK_EQ([notification object], window);
+ if ([window isVisible]) {
+ // If the window isn't visible, it is already closed, and this notification
+ // has been sent as part of the closing operation, so no need to close.
+ [self ok:self];
+ }
+}
+
+// Look at the dialog; if the user has changed anything, update the
+// bookmark node to reflect this.
+- (void)updateBookmarkNode {
+ if (!node_) return;
+
+ // First the title...
+ NSString* oldTitle = base::SysUTF16ToNSString(node_->GetTitle());
+ NSString* newTitle = [nameTextField_ stringValue];
+ if (![oldTitle isEqual:newTitle]) {
+ model_->SetTitle(node_, base::SysNSStringToUTF16(newTitle));
+ UserMetrics::RecordAction(
+ UserMetricsAction("BookmarkBubble_ChangeTitleInBubble"),
+ model_->profile());
+ }
+ // Then the parent folder.
+ const BookmarkNode* oldParent = node_->GetParent();
+ NSMenuItem* selectedItem = [folderPopUpButton_ selectedItem];
+ id representedObject = [selectedItem representedObject];
+ if ([representedObject isEqual:[[self class] chooseAnotherFolderObject]]) {
+ // "Choose another folder..."
+ return;
+ }
+ const BookmarkNode* newParent =
+ static_cast<const BookmarkNode*>([representedObject pointerValue]);
+ DCHECK(newParent);
+ if (oldParent != newParent) {
+ int index = newParent->GetChildCount();
+ model_->Move(node_, newParent, index);
+ UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_ChangeParent"),
+ model_->profile());
+ }
+}
+
+// Fill in all information related to the folder pop up button.
+- (void)fillInFolderList {
+ [nameTextField_ setStringValue:base::SysUTF16ToNSString(node_->GetTitle())];
+ DCHECK([folderPopUpButton_ numberOfItems] == 0);
+ [self addFolderNodes:model_->root_node()
+ toPopUpButton:folderPopUpButton_
+ indentation:0];
+ NSMenu* menu = [folderPopUpButton_ menu];
+ NSString* title = [[self class] chooseAnotherFolderString];
+ NSMenuItem *item = [menu addItemWithTitle:title
+ action:NULL
+ keyEquivalent:@""];
+ ChooseAnotherFolder* obj = [[self class] chooseAnotherFolderObject];
+ [item setRepresentedObject:obj];
+ // Finally, select the current parent.
+ NSValue* parentValue = [NSValue valueWithPointer:node_->GetParent()];
+ NSInteger idx = [menu indexOfItemWithRepresentedObject:parentValue];
+ [folderPopUpButton_ selectItemAtIndex:idx];
+}
+
+@end // BookmarkBubbleController
+
+
+@implementation BookmarkBubbleController(ExposedForUnitTesting)
+
++ (NSString*)chooseAnotherFolderString {
+ return l10n_util::GetNSStringWithFixup(
+ IDS_BOOMARK_BUBBLE_CHOOSER_ANOTHER_FOLDER);
+}
+
+// For the given folder node, walk the tree and add folder names to
+// the given pop up button.
+- (void)addFolderNodes:(const BookmarkNode*)parent
+ toPopUpButton:(NSPopUpButton*)button
+ indentation:(int)indentation {
+ if (!model_->is_root(parent)) {
+ NSString* title = base::SysUTF16ToNSString(parent->GetTitle());
+ NSMenu* menu = [button menu];
+ NSMenuItem* item = [menu addItemWithTitle:title
+ action:NULL
+ keyEquivalent:@""];
+ [item setRepresentedObject:[NSValue valueWithPointer:parent]];
+ [item setIndentationLevel:indentation];
+ ++indentation;
+ }
+ for (int i = 0; i < parent->GetChildCount(); i++) {
+ const BookmarkNode* child = parent->GetChild(i);
+ if (child->is_folder())
+ [self addFolderNodes:child
+ toPopUpButton:button
+ indentation:indentation];
+ }
+}
+
+- (void)setTitle:(NSString*)title parentFolder:(const BookmarkNode*)parent {
+ [nameTextField_ setStringValue:title];
+ [self setParentFolderSelection:parent];
+}
+
+// Pick a specific parent node in the selection by finding the right
+// pop up button index.
+- (void)setParentFolderSelection:(const BookmarkNode*)parent {
+ // Expectation: There is a parent mapping for all items in the
+ // folderPopUpButton except the last one ("Choose another folder...").
+ NSMenu* menu = [folderPopUpButton_ menu];
+ NSValue* parentValue = [NSValue valueWithPointer:parent];
+ NSInteger idx = [menu indexOfItemWithRepresentedObject:parentValue];
+ DCHECK(idx != -1);
+ [folderPopUpButton_ selectItemAtIndex:idx];
+}
+
+- (NSPopUpButton*)folderPopUpButton {
+ return folderPopUpButton_;
+}
+
+@end // implementation BookmarkBubbleController(ExposedForUnitTesting)
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller_unittest.mm
new file mode 100644
index 0000000..ef3a47a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller_unittest.mm
@@ -0,0 +1,490 @@
+// 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"
+#include "base/utf_string_conversions.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/info_bubble_window.h"
+#include "chrome/common/notification_service.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+// Watch for bookmark pulse notifications so we can confirm they were sent.
+@interface BookmarkPulseObserver : NSObject {
+ int notifications_;
+}
+@property (assign, nonatomic) int notifications;
+@end
+
+
+@implementation BookmarkPulseObserver
+
+@synthesize notifications = notifications_;
+
+- (id)init {
+ if ((self = [super init])) {
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(pulseBookmarkNotification:)
+ name:bookmark_button::kPulseBookmarkButtonNotification
+ object:nil];
+ }
+ return self;
+}
+
+- (void)pulseBookmarkNotification:(NSNotificationCenter *)notification {
+ notifications_++;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+@end
+
+
+namespace {
+
+class BookmarkBubbleControllerTest : public CocoaTest {
+ public:
+ static int edits_;
+ BrowserTestHelper helper_;
+ BookmarkBubbleController* controller_;
+
+ BookmarkBubbleControllerTest() : controller_(nil) {
+ edits_ = 0;
+ }
+
+ virtual void TearDown() {
+ [controller_ close];
+ CocoaTest::TearDown();
+ }
+
+ // Returns a controller but ownership not transferred.
+ // Only one of these will be valid at a time.
+ BookmarkBubbleController* ControllerForNode(const BookmarkNode* node) {
+ if (controller_ && !IsWindowClosing()) {
+ [controller_ close];
+ controller_ = nil;
+ }
+ controller_ = [[BookmarkBubbleController alloc]
+ initWithParentWindow:test_window()
+ model:helper_.profile()->GetBookmarkModel()
+ node:node
+ alreadyBookmarked:YES];
+ EXPECT_TRUE([controller_ window]);
+ // The window must be gone or we'll fail a unit test with windows left open.
+ [static_cast<InfoBubbleWindow*>([controller_ window]) setDelayOnClose:NO];
+ [controller_ showWindow:nil];
+ return controller_;
+ }
+
+ BookmarkModel* GetBookmarkModel() {
+ return helper_.profile()->GetBookmarkModel();
+ }
+
+ bool IsWindowClosing() {
+ return [static_cast<InfoBubbleWindow*>([controller_ window]) isClosing];
+ }
+};
+
+// static
+int BookmarkBubbleControllerTest::edits_;
+
+// Confirm basics about the bubble window (e.g. that it is inside the
+// parent window)
+TEST_F(BookmarkBubbleControllerTest, TestBubbleWindow) {
+ BookmarkModel* model = GetBookmarkModel();
+ const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(),
+ 0,
+ ASCIIToUTF16("Bookie markie title"),
+ GURL("http://www.google.com"));
+ BookmarkBubbleController* controller = ControllerForNode(node);
+ EXPECT_TRUE(controller);
+ NSWindow* window = [controller window];
+ EXPECT_TRUE(window);
+ EXPECT_TRUE(NSContainsRect([test_window() frame],
+ [window frame]));
+}
+
+// Test that we can handle closing the parent window
+TEST_F(BookmarkBubbleControllerTest, TestClosingParentWindow) {
+ BookmarkModel* model = GetBookmarkModel();
+ const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(),
+ 0,
+ ASCIIToUTF16("Bookie markie title"),
+ GURL("http://www.google.com"));
+ BookmarkBubbleController* controller = ControllerForNode(node);
+ EXPECT_TRUE(controller);
+ NSWindow* window = [controller window];
+ EXPECT_TRUE(window);
+ base::mac::ScopedNSAutoreleasePool pool;
+ [test_window() performClose:NSApp];
+}
+
+
+// Confirm population of folder list
+TEST_F(BookmarkBubbleControllerTest, TestFillInFolder) {
+ // Create some folders, including a nested folder
+ BookmarkModel* model = GetBookmarkModel();
+ EXPECT_TRUE(model);
+ const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode();
+ EXPECT_TRUE(bookmarkBarNode);
+ const BookmarkNode* node1 = model->AddGroup(bookmarkBarNode, 0,
+ ASCIIToUTF16("one"));
+ EXPECT_TRUE(node1);
+ const BookmarkNode* node2 = model->AddGroup(bookmarkBarNode, 1,
+ ASCIIToUTF16("two"));
+ EXPECT_TRUE(node2);
+ const BookmarkNode* node3 = model->AddGroup(bookmarkBarNode, 2,
+ ASCIIToUTF16("three"));
+ EXPECT_TRUE(node3);
+ const BookmarkNode* node4 = model->AddGroup(node2, 0, ASCIIToUTF16("sub"));
+ EXPECT_TRUE(node4);
+ const BookmarkNode* node5 = model->AddURL(node1, 0, ASCIIToUTF16("title1"),
+ GURL("http://www.google.com"));
+ EXPECT_TRUE(node5);
+ const BookmarkNode* node6 = model->AddURL(node3, 0, ASCIIToUTF16("title2"),
+ GURL("http://www.google.com"));
+ EXPECT_TRUE(node6);
+ const BookmarkNode* node7 = model->AddURL(node4, 0, ASCIIToUTF16("title3"),
+ GURL("http://www.google.com/reader"));
+ EXPECT_TRUE(node7);
+
+ BookmarkBubbleController* controller = ControllerForNode(node4);
+ EXPECT_TRUE(controller);
+
+ NSArray* titles =
+ [[[controller folderPopUpButton] itemArray] valueForKey:@"title"];
+ EXPECT_TRUE([titles containsObject:@"one"]);
+ EXPECT_TRUE([titles containsObject:@"two"]);
+ EXPECT_TRUE([titles containsObject:@"three"]);
+ EXPECT_TRUE([titles containsObject:@"sub"]);
+ EXPECT_FALSE([titles containsObject:@"title1"]);
+ EXPECT_FALSE([titles containsObject:@"title2"]);
+}
+
+// Confirm ability to handle folders with blank name.
+TEST_F(BookmarkBubbleControllerTest, TestFolderWithBlankName) {
+ // Create some folders, including a nested folder
+ BookmarkModel* model = GetBookmarkModel();
+ EXPECT_TRUE(model);
+ const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode();
+ EXPECT_TRUE(bookmarkBarNode);
+ const BookmarkNode* node1 = model->AddGroup(bookmarkBarNode, 0,
+ ASCIIToUTF16("one"));
+ EXPECT_TRUE(node1);
+ const BookmarkNode* node2 = model->AddGroup(bookmarkBarNode, 1,
+ ASCIIToUTF16(""));
+ EXPECT_TRUE(node2);
+ const BookmarkNode* node3 = model->AddGroup(bookmarkBarNode, 2,
+ ASCIIToUTF16("three"));
+ EXPECT_TRUE(node3);
+ const BookmarkNode* node2_1 = model->AddURL(node2, 0, ASCIIToUTF16("title1"),
+ GURL("http://www.google.com"));
+ EXPECT_TRUE(node2_1);
+
+ BookmarkBubbleController* controller = ControllerForNode(node1);
+ EXPECT_TRUE(controller);
+
+ // One of the items should be blank and its node should be node2.
+ NSArray* items = [[controller folderPopUpButton] itemArray];
+ EXPECT_GT([items count], 4U);
+ BOOL blankFolderFound = NO;
+ for (NSMenuItem* item in [[controller folderPopUpButton] itemArray]) {
+ if ([[item title] length] == 0 &&
+ static_cast<const BookmarkNode*>([[item representedObject]
+ pointerValue]) == node2) {
+ blankFolderFound = YES;
+ break;
+ }
+ }
+ EXPECT_TRUE(blankFolderFound);
+}
+
+
+// Click on edit; bubble gets closed.
+TEST_F(BookmarkBubbleControllerTest, TestEdit) {
+ BookmarkModel* model = GetBookmarkModel();
+ const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(),
+ 0,
+ ASCIIToUTF16("Bookie markie title"),
+ GURL("http://www.google.com"));
+ BookmarkBubbleController* controller = ControllerForNode(node);
+ EXPECT_TRUE(controller);
+
+ EXPECT_EQ(edits_, 0);
+ EXPECT_FALSE(IsWindowClosing());
+ [controller edit:controller];
+ EXPECT_EQ(edits_, 1);
+ EXPECT_TRUE(IsWindowClosing());
+}
+
+// CallClose; bubble gets closed.
+// Also confirm pulse notifications get sent.
+TEST_F(BookmarkBubbleControllerTest, TestClose) {
+ BookmarkModel* model = GetBookmarkModel();
+ const BookmarkNode* node = model->AddURL(
+ model->GetBookmarkBarNode(), 0, ASCIIToUTF16("Bookie markie title"),
+ GURL("http://www.google.com"));
+ EXPECT_EQ(edits_, 0);
+
+ scoped_nsobject<BookmarkPulseObserver> observer([[BookmarkPulseObserver alloc]
+ init]);
+ EXPECT_EQ([observer notifications], 0);
+ BookmarkBubbleController* controller = ControllerForNode(node);
+ EXPECT_TRUE(controller);
+ EXPECT_FALSE(IsWindowClosing());
+ EXPECT_EQ([observer notifications], 1);
+ [controller ok:controller];
+ EXPECT_EQ(edits_, 0);
+ EXPECT_TRUE(IsWindowClosing());
+ EXPECT_EQ([observer notifications], 2);
+}
+
+// User changes title and parent folder in the UI
+TEST_F(BookmarkBubbleControllerTest, TestUserEdit) {
+ BookmarkModel* model = GetBookmarkModel();
+ EXPECT_TRUE(model);
+ const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode();
+ EXPECT_TRUE(bookmarkBarNode);
+ const BookmarkNode* node = model->AddURL(bookmarkBarNode,
+ 0,
+ ASCIIToUTF16("short-title"),
+ GURL("http://www.google.com"));
+ const BookmarkNode* grandma = model->AddGroup(bookmarkBarNode, 0,
+ ASCIIToUTF16("grandma"));
+ EXPECT_TRUE(grandma);
+ const BookmarkNode* grandpa = model->AddGroup(bookmarkBarNode, 0,
+ ASCIIToUTF16("grandpa"));
+ EXPECT_TRUE(grandpa);
+
+ BookmarkBubbleController* controller = ControllerForNode(node);
+ EXPECT_TRUE(controller);
+
+ // simulate a user edit
+ [controller setTitle:@"oops" parentFolder:grandma];
+ [controller edit:controller];
+
+ // Make sure bookmark has changed
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("oops"));
+ EXPECT_EQ(node->GetParent()->GetTitle(), ASCIIToUTF16("grandma"));
+}
+
+// Confirm happiness with parent nodes that have the same name.
+TEST_F(BookmarkBubbleControllerTest, TestNewParentSameName) {
+ BookmarkModel* model = GetBookmarkModel();
+ EXPECT_TRUE(model);
+ const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode();
+ EXPECT_TRUE(bookmarkBarNode);
+ for (int i=0; i<2; i++) {
+ const BookmarkNode* node = model->AddURL(bookmarkBarNode,
+ 0,
+ ASCIIToUTF16("short-title"),
+ GURL("http://www.google.com"));
+ EXPECT_TRUE(node);
+ const BookmarkNode* group = model->AddGroup(bookmarkBarNode, 0,
+ ASCIIToUTF16("NAME"));
+ EXPECT_TRUE(group);
+ group = model->AddGroup(bookmarkBarNode, 0, ASCIIToUTF16("NAME"));
+ EXPECT_TRUE(group);
+ group = model->AddGroup(bookmarkBarNode, 0, ASCIIToUTF16("NAME"));
+ EXPECT_TRUE(group);
+ BookmarkBubbleController* controller = ControllerForNode(node);
+ EXPECT_TRUE(controller);
+
+ // simulate a user edit
+ [controller setParentFolderSelection:bookmarkBarNode->GetChild(i)];
+ [controller edit:controller];
+
+ // Make sure bookmark has changed, and that the parent is what we
+ // expect. This proves nobody did searching based on name.
+ EXPECT_EQ(node->GetParent(), bookmarkBarNode->GetChild(i));
+ }
+}
+
+// Confirm happiness with nodes with the same Name
+TEST_F(BookmarkBubbleControllerTest, TestDuplicateNodeNames) {
+ BookmarkModel* model = GetBookmarkModel();
+ const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode();
+ EXPECT_TRUE(bookmarkBarNode);
+ const BookmarkNode* node1 = model->AddGroup(bookmarkBarNode, 0,
+ ASCIIToUTF16("NAME"));
+ EXPECT_TRUE(node1);
+ const BookmarkNode* node2 = model->AddGroup(bookmarkBarNode, 0,
+ ASCIIToUTF16("NAME"));
+ EXPECT_TRUE(node2);
+ BookmarkBubbleController* controller = ControllerForNode(bookmarkBarNode);
+ EXPECT_TRUE(controller);
+
+ NSPopUpButton* button = [controller folderPopUpButton];
+ [controller setParentFolderSelection:node1];
+ NSMenuItem* item = [button selectedItem];
+ id itemObject = [item representedObject];
+ EXPECT_NSEQ([NSValue valueWithPointer:node1], itemObject);
+ [controller setParentFolderSelection:node2];
+ item = [button selectedItem];
+ itemObject = [item representedObject];
+ EXPECT_NSEQ([NSValue valueWithPointer:node2], itemObject);
+}
+
+// Click the "remove" button
+TEST_F(BookmarkBubbleControllerTest, TestRemove) {
+ BookmarkModel* model = GetBookmarkModel();
+ GURL gurl("http://www.google.com");
+ const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(),
+ 0,
+ ASCIIToUTF16("Bookie markie title"),
+ gurl);
+ BookmarkBubbleController* controller = ControllerForNode(node);
+ EXPECT_TRUE(controller);
+ EXPECT_TRUE(model->IsBookmarked(gurl));
+
+ [controller remove:controller];
+ EXPECT_FALSE(model->IsBookmarked(gurl));
+ EXPECT_TRUE(IsWindowClosing());
+}
+
+// Confirm picking "choose another folder" caused edit: to be called.
+TEST_F(BookmarkBubbleControllerTest, PopUpSelectionChanged) {
+ BookmarkModel* model = GetBookmarkModel();
+ GURL gurl("http://www.google.com");
+ const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(),
+ 0, ASCIIToUTF16("super-title"),
+ gurl);
+ BookmarkBubbleController* controller = ControllerForNode(node);
+ EXPECT_TRUE(controller);
+
+ NSPopUpButton* button = [controller folderPopUpButton];
+ [button selectItemWithTitle:[[controller class] chooseAnotherFolderString]];
+ EXPECT_EQ(edits_, 0);
+ [button sendAction:[button action] to:[button target]];
+ EXPECT_EQ(edits_, 1);
+}
+
+// Create a controller that simulates the bookmark just now being created by
+// the user clicking the star, then sending the "cancel" command to represent
+// them pressing escape. The bookmark should not be there.
+TEST_F(BookmarkBubbleControllerTest, EscapeRemovesNewBookmark) {
+ BookmarkModel* model = GetBookmarkModel();
+ GURL gurl("http://www.google.com");
+ const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(),
+ 0,
+ ASCIIToUTF16("Bookie markie title"),
+ gurl);
+ BookmarkBubbleController* controller =
+ [[BookmarkBubbleController alloc]
+ initWithParentWindow:test_window()
+ model:helper_.profile()->GetBookmarkModel()
+ node:node
+ alreadyBookmarked:NO]; // The last param is the key difference.
+ EXPECT_TRUE([controller window]);
+ // Calls release on controller.
+ [controller cancel:nil];
+ EXPECT_FALSE(model->IsBookmarked(gurl));
+}
+
+// Create a controller where the bookmark already existed prior to clicking
+// the star and test that sending a cancel command doesn't change the state
+// of the bookmark.
+TEST_F(BookmarkBubbleControllerTest, EscapeDoesntTouchExistingBookmark) {
+ BookmarkModel* model = GetBookmarkModel();
+ GURL gurl("http://www.google.com");
+ const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(),
+ 0,
+ ASCIIToUTF16("Bookie markie title"),
+ gurl);
+ BookmarkBubbleController* controller = ControllerForNode(node);
+ EXPECT_TRUE(controller);
+
+ [(id)controller cancel:nil];
+ EXPECT_TRUE(model->IsBookmarked(gurl));
+}
+
+// Confirm indentation of items in pop-up menu
+TEST_F(BookmarkBubbleControllerTest, TestMenuIndentation) {
+ // Create some folders, including a nested folder
+ BookmarkModel* model = GetBookmarkModel();
+ EXPECT_TRUE(model);
+ const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode();
+ EXPECT_TRUE(bookmarkBarNode);
+ const BookmarkNode* node1 = model->AddGroup(bookmarkBarNode, 0,
+ ASCIIToUTF16("one"));
+ EXPECT_TRUE(node1);
+ const BookmarkNode* node2 = model->AddGroup(bookmarkBarNode, 1,
+ ASCIIToUTF16("two"));
+ EXPECT_TRUE(node2);
+ const BookmarkNode* node2_1 = model->AddGroup(node2, 0,
+ ASCIIToUTF16("two dot one"));
+ EXPECT_TRUE(node2_1);
+ const BookmarkNode* node3 = model->AddGroup(bookmarkBarNode, 2,
+ ASCIIToUTF16("three"));
+ EXPECT_TRUE(node3);
+
+ BookmarkBubbleController* controller = ControllerForNode(node1);
+ EXPECT_TRUE(controller);
+
+ // Compare the menu item indents against expectations.
+ static const int kExpectedIndent[] = {0, 1, 1, 2, 1, 0};
+ NSArray* items = [[controller folderPopUpButton] itemArray];
+ ASSERT_GE([items count], 6U);
+ for(int itemNo = 0; itemNo < 6; itemNo++) {
+ NSMenuItem* item = [items objectAtIndex:itemNo];
+ EXPECT_EQ(kExpectedIndent[itemNo], [item indentationLevel])
+ << "Unexpected indent for menu item #" << itemNo;
+ }
+}
+
+// Confirm bubble goes away when a new tab is created.
+TEST_F(BookmarkBubbleControllerTest, BubbleGoesAwayOnNewTab) {
+
+ BookmarkModel* model = GetBookmarkModel();
+ const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(),
+ 0,
+ ASCIIToUTF16("Bookie markie title"),
+ GURL("http://www.google.com"));
+ EXPECT_EQ(edits_, 0);
+
+ BookmarkBubbleController* controller = ControllerForNode(node);
+ EXPECT_TRUE(controller);
+ EXPECT_FALSE(IsWindowClosing());
+
+ // We can't actually create a new tab here, e.g.
+ // helper_.browser()->AddTabWithURL(...);
+ // Many of our browser objects (Browser, Profile, RequestContext)
+ // are "just enough" to run tests without being complete. Instead
+ // we fake the notification that would be triggered by a tab
+ // creation.
+ NotificationService::current()->Notify(
+ NotificationType::TAB_CONTENTS_CONNECTED,
+ Source<TabContentsDelegate>(NULL),
+ Details<TabContents>(NULL));
+
+ // Confirm bubble going bye-bye.
+ EXPECT_TRUE(IsWindowClosing());
+}
+
+
+} // namespace
+
+@implementation NSApplication (BookmarkBubbleUnitTest)
+// Add handler for the editBookmarkNode: action to NSApp for testing purposes.
+// Normally this would be sent up the responder tree correctly, but since
+// tests run in the background, key window and main window are never set on
+// NSApplication. Adding it to NSApplication directly removes the need for
+// worrying about what the current window with focus is.
+- (void)editBookmarkNode:(id)sender {
+ EXPECT_TRUE([sender respondsToSelector:@selector(node)]);
+ BookmarkBubbleControllerTest::edits_++;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_button.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_button.h
new file mode 100644
index 0000000..0bea5a5e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_button.h
@@ -0,0 +1,243 @@
+// 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 <vector>
+#import "chrome/browser/ui/cocoa/draggable_button.h"
+#include "webkit/glue/window_open_disposition.h"
+
+@class BookmarkBarFolderController;
+@class BookmarkButton;
+struct BookmarkNodeData;
+class BookmarkModel;
+class BookmarkNode;
+@class BrowserWindowController;
+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;
+
+// Returns YES if a drag operation should lock the fullscreen overlay bar
+// visibility before starting. For example, dragging a bookmark button should
+// not lock the overlay if the bookmark bar is currently showing in detached
+// mode on the NTP.
+- (BOOL)dragShouldLockBarVisibility;
+
+// Returns the top-level window for this button.
+- (NSWindow*)browserWindow;
+
+// Returns YES if the bookmark button can be dragged to the trash, NO otherwise.
+- (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button;
+
+// This is called after the user has dropped the bookmark button on the trash.
+// The delegate can use this event to delete the bookmark.
+- (void)didDragBookmarkToTrash:(BookmarkButton*)button;
+
+@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;
+
+// Returns YES if a drag operation should lock the fullscreen overlay bar
+// visibility before starting. For example, dragging a bookmark button should
+// not lock the overlay if the bookmark bar is currently showing in detached
+// mode on the NTP.
+- (BOOL)dragShouldLockBarVisibility;
+
+// Perform the actual DnD of a bookmark or bookmark button.
+
+// |point| is in the base coordinate system of the destination window;
+// |it comes from an id<NSDraggingInfo>. |copy| is YES if a copy is to be
+// made and inserted into the new location while leaving the bookmark in
+// the old location, otherwise move the bookmark by removing from its old
+// location and inserting into the new location.
+- (BOOL)dragButton:(BookmarkButton*)sourceButton
+ to:(NSPoint)point
+ copy:(BOOL)copy;
+
+// Determine if the pasteboard from |info| has dragging data containing
+// bookmark(s) and perform the drag and return YES, otherwise return NO.
+- (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info;
+
+// Determine if the drag pasteboard has any drag data of type
+// kBookmarkDictionaryListPboardType and, if so, return those elements
+// otherwise return an empty vector.
+- (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData;
+
+// 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)indicatorPosForDragToPoint:(NSPoint)point;
+
+// Return the theme provider associated with this browser window.
+- (ThemeProvider*)themeProvider;
+
+// Called just before a child folder puts itself on screen.
+- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child;
+
+// Called just before a child folder closes.
+- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child;
+
+// Return a controller's folder controller for a subfolder, or nil.
+- (BookmarkBarFolderController*)folderController;
+
+// Add a new folder controller as triggered by the given folder button.
+// If there is a current folder controller, close it.
+- (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton;
+
+// Open all of the nodes for the given node with disposition.
+- (void)openAll:(const BookmarkNode*)node
+ disposition:(WindowOpenDisposition)disposition;
+
+// There are several operations which may affect the contents of a bookmark
+// button controller after it has been created, primary of which are
+// cut/paste/delete and drag/drop. Such changes may involve coordinating
+// the bookmark button contents of two controllers (such as when a bookmark is
+// dragged from one folder to another). The bookmark bar controller
+// coordinates in response to notifications propogated by the bookmark model
+// through BookmarkBarBridge calls. The following three functions are
+// implemented by the controllers and are dispatched by the bookmark bar
+// controller in response to notifications coming in from the BookmarkBarBridge.
+
+// Add a button for the given node to the bar or folder menu. This is safe
+// to call when a folder menu window is open as that window will be updated.
+// And index of -1 means to append to the end (bottom).
+- (void)addButtonForNode:(const BookmarkNode*)node
+ atIndex:(NSInteger)buttonIndex;
+
+// Given a list or |urls| and |titles|, create new bookmark nodes and add
+// them to the bookmark model such that they will be 1) added to the folder
+// represented by the button at |point| if it is a folder, or 2) inserted
+// into the parent of the non-folder bookmark at |point| in front of that
+// button. Returns YES if at least one bookmark was added.
+// TODO(mrossetti): Change function to use a pair-like structure for
+// URLs and titles. http://crbug.com/44411
+- (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point;
+
+// Move a button from one place in the menu to another. This is safe
+// to call when a folder menu window is open as that window will be updated.
+- (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex;
+
+// Remove the bookmark button at the given index. Show the poof animation
+// if |animate:| is YES. It may be obvious, but this is safe
+// to call when a folder menu window is open as that window will be updated.
+- (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)poof;
+
+// Determine the controller containing the button representing |node|, if any.
+- (id<BookmarkButtonControllerProtocol>)controllerForNode:
+ (const BookmarkNode*)node;
+
+@end // @protocol BookmarkButtonControllerProtocol
+
+
+// Class for bookmark bar buttons that can be drag sources.
+@interface BookmarkButton : DraggableButton {
+ @private
+ IBOutlet NSObject<BookmarkButtonDelegate>* delegate_; // Weak.
+
+ // Saved pointer to the BWC for the browser window that contains this button.
+ // Used to lock and release bar visibility during a drag. The pointer is
+ // saved because the bookmark button is no longer a part of a window at the
+ // end of a drag operation (or, in fact, can be dragged to a completely
+ // different window), so there is no way to retrieve the same BWC object after
+ // a drag.
+ BrowserWindowController* visibilityDelegate_; // weak
+
+ NSPoint dragMouseOffset_;
+ NSPoint dragEndScreenLocation_;
+ BOOL dragPending_;
+}
+
+@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;
+
+// Turn on or off pulsing of a bookmark button.
+// Triggered by the bookmark bubble.
+- (void)setIsContinuousPulsing:(BOOL)flag;
+
+// Return continuous pulse state.
+- (BOOL)isContinuousPulsing;
+
+// Return the location in screen coordinates where the remove animation should
+// be displayed.
+- (NSPoint)screenLocationForRemoveAnimation;
+
+@end // @interface BookmarkButton
+
+
+@interface BookmarkButton(TestingAPI)
+- (void)beginDrag:(NSEvent*)event;
+@end
+
+namespace bookmark_button {
+
+// Notifications for pulsing of bookmarks.
+extern NSString* const kPulseBookmarkButtonNotification;
+
+// Key for userInfo dict of a kPulseBookmarkButtonNotification.
+// Value is a [NSValue valueWithPointer:]; pointer is a (const BookmarkNode*).
+extern NSString* const kBookmarkKey;
+
+// Key for userInfo dict of a kPulseBookmarkButtonNotification.
+// Value is a [NSNumber numberWithBool:] to turn pulsing on or off.
+extern NSString* const kBookmarkPulseFlagKey;
+
+};
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_button.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_button.mm
new file mode 100644
index 0000000..885e5c8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_button.mm
@@ -0,0 +1,238 @@
+// 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/ui/cocoa/bookmarks/bookmark_button.h"
+
+#include "base/logging.h"
+#import "base/scoped_nsobject.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/metrics/user_metrics.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+
+// The opacity of the bookmark button drag image.
+static const CGFloat kDragImageOpacity = 0.7;
+
+
+namespace bookmark_button {
+
+NSString* const kPulseBookmarkButtonNotification =
+ @"PulseBookmarkButtonNotification";
+NSString* const kBookmarkKey = @"BookmarkKey";
+NSString* const kBookmarkPulseFlagKey = @"BookmarkPulseFlagKey";
+
+};
+
+@interface BookmarkButton(Private)
+
+// Make a drag image for the button.
+- (NSImage*)dragImage;
+
+@end // @interface BookmarkButton(Private)
+
+
+@implementation BookmarkButton
+
+@synthesize delegate = delegate_;
+
+- (id)initWithFrame:(NSRect)frameRect {
+ // BookmarkButton's ViewID may be changed to VIEW_ID_OTHER_BOOKMARKS in
+ // BookmarkBarController, so we can't just override -viewID method to return
+ // it.
+ if ((self = [super initWithFrame:frameRect]))
+ view_id_util::SetID(self, VIEW_ID_BOOKMARK_BAR_ELEMENT);
+ return self;
+}
+
+- (void)dealloc {
+ if ([[self cell] respondsToSelector:@selector(safelyStopPulsing)])
+ [[self cell] safelyStopPulsing];
+ view_id_util::UnsetID(self);
+ [super dealloc];
+}
+
+- (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;
+}
+
+- (void)setIsContinuousPulsing:(BOOL)flag {
+ [[self cell] setIsContinuousPulsing:flag];
+}
+
+- (BOOL)isContinuousPulsing {
+ return [[self cell] isContinuousPulsing];
+}
+
+- (NSPoint)screenLocationForRemoveAnimation {
+ NSPoint point;
+
+ if (dragPending_) {
+ // Use the position of the mouse in the drag image as the location.
+ point = dragEndScreenLocation_;
+ point.x += dragMouseOffset_.x;
+ if ([self isFlipped]) {
+ point.y += [self bounds].size.height - dragMouseOffset_.y;
+ } else {
+ point.y += dragMouseOffset_.y;
+ }
+ } else {
+ // Use the middle of this button as the location.
+ NSRect bounds = [self bounds];
+ point = NSMakePoint(NSMidX(bounds), NSMidY(bounds));
+ point = [self convertPoint:point toView:nil];
+ point = [[self window] convertBaseToScreen:point];
+ }
+
+ return point;
+}
+
+// By default, NSButton ignores middle-clicks.
+// But we want them.
+- (void)otherMouseUp:(NSEvent*)event {
+ [self performClick:self];
+}
+
+// Overridden from DraggableButton.
+- (void)beginDrag:(NSEvent*)event {
+ // 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]) {
+ NOTREACHED();
+ return;
+ }
+ // Ask our delegate to fill the pasteboard for us.
+ NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
+ [[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
+ // the stack.
+ [self retain];
+
+ // Lock bar visibility, forcing the overlay to stay visible if we are in
+ // fullscreen mode.
+ if ([[self delegate] dragShouldLockBarVisibility]) {
+ DCHECK(!visibilityDelegate_);
+ NSWindow* window = [[self delegate] browserWindow];
+ visibilityDelegate_ =
+ [BrowserWindowController browserWindowControllerForWindow:window];
+ [visibilityDelegate_ lockBarVisibilityForOwner:self
+ withAnimation:NO
+ delay:NO];
+ }
+ const BookmarkNode* node = [self bookmarkNode];
+ const BookmarkNode* parent = node ? node->GetParent() : NULL;
+ if (parent && parent->type() == BookmarkNode::FOLDER) {
+ UserMetrics::RecordAction(UserMetricsAction("BookmarkBarFolder_DragStart"));
+ } else {
+ UserMetrics::RecordAction(UserMetricsAction("BookmarkBar_DragStart"));
+ }
+
+ dragMouseOffset_ = [self convertPointFromBase:[event locationInWindow]];
+ dragPending_ = YES;
+
+ CGFloat yAt = [self bounds].size.height;
+ NSSize dragOffset = NSMakeSize(0.0, 0.0);
+ [self dragImage:[self dragImage] at:NSMakePoint(0, yAt) offset:dragOffset
+ event:event pasteboard:pboard source:self slideBack:YES];
+
+ // And we're done.
+ dragPending_ = NO;
+ [self autorelease];
+}
+
+// Overridden to release bar visibility.
+- (void)endDrag {
+ // visibilityDelegate_ can be nil if we're detached, and that's fine.
+ [visibilityDelegate_ releaseBarVisibilityForOwner:self
+ withAnimation:YES
+ delay:YES];
+ visibilityDelegate_ = nil;
+ [super endDrag];
+}
+
+- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
+ NSDragOperation operation = NSDragOperationCopy;
+ if (isLocal) {
+ operation |= NSDragOperationMove;
+ }
+ if ([delegate_ canDragBookmarkButtonToTrash:self]) {
+ operation |= NSDragOperationDelete;
+ }
+ return operation;
+}
+
+- (void)draggedImage:(NSImage *)anImage
+ endedAt:(NSPoint)aPoint
+ operation:(NSDragOperation)operation {
+ if (operation & NSDragOperationDelete) {
+ dragEndScreenLocation_ = aPoint;
+ [delegate_ didDragBookmarkToTrash:self];
+ }
+}
+
+// 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];
+}
+
+// See comments above mouseEntered:.
+- (void)mouseExited:(NSEvent*)event {
+ [delegate_ mouseExitedButton:self event:event];
+}
+
+@end
+
+@implementation BookmarkButton(Private)
+
+- (NSImage*)dragImage {
+ NSRect bounds = [self bounds];
+
+ // Grab the image from the screen and put it in an |NSImage|. We can't use
+ // this directly since we need to clip it and set its opacity. This won't work
+ // if the source view is clipped. Fortunately, we don't display clipped
+ // bookmark buttons.
+ [self lockFocus];
+ scoped_nsobject<NSBitmapImageRep>
+ bitmap([[NSBitmapImageRep alloc] initWithFocusedViewRect:bounds]);
+ [self unlockFocus];
+ scoped_nsobject<NSImage> image([[NSImage alloc] initWithSize:[bitmap size]]);
+ [image addRepresentation:bitmap];
+
+ // Make an autoreleased |NSImage|, which will be returned, and draw into it.
+ // By default, the |NSImage| will be completely transparent.
+ NSImage* dragImage =
+ [[[NSImage alloc] initWithSize:[bitmap size]] autorelease];
+ [dragImage lockFocus];
+
+ // Draw the image with the appropriate opacity, clipping it tightly.
+ GradientButtonCell* cell = static_cast<GradientButtonCell*>([self cell]);
+ DCHECK([cell isKindOfClass:[GradientButtonCell class]]);
+ [[cell clipPathForFrame:bounds inView:self] setClip];
+ [image drawAtPoint:NSMakePoint(0, 0)
+ fromRect:NSMakeRect(0, 0, NSWidth(bounds), NSHeight(bounds))
+ operation:NSCompositeSourceOver
+ fraction:kDragImageOpacity];
+
+ [dragImage unlockFocus];
+ return dragImage;
+}
+
+@end // @implementation BookmarkButton(Private)
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h
new file mode 100644
index 0000000..e126ac3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h
@@ -0,0 +1,65 @@
+// 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_UI_COCOA_BOOKMARKS_BOOKMARK_BUTTON_CELL_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BUTTON_CELL_H_
+#pragma once
+
+#import "base/cocoa_protocols_mac.h"
+#import "chrome/browser/ui/cocoa/gradient_button_cell.h"
+
+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?
+
+ // Starting index of bookmarkFolder children that we care to use.
+ int startingChildIndex_;
+
+ // Should we draw the folder arrow as needed? Not used for the bar
+ // itself but used on the folder windows.
+ BOOL drawFolderArrow_;
+
+ // Arrow for folders
+ scoped_nsobject<NSImage> arrowImage_;
+}
+
+@property (nonatomic, readwrite, assign) const BookmarkNode* bookmarkNode;
+@property (nonatomic, readwrite, assign) int startingChildIndex;
+@property (nonatomic, readwrite, assign) BOOL drawFolderArrow;
+
+// Create a button cell which draws with a theme.
++ (id)buttonCellForNode:(const BookmarkNode*)node
+ contextMenu:(NSMenu*)contextMenu
+ cellText:(NSString*)cellText
+ cellImage:(NSImage*)cellImage;
+
+// Initialize a button cell which draws with a theme.
+// Designated initializer.
+- (id)initForNode:(const BookmarkNode*)node
+ contextMenu:(NSMenu*)contextMenu
+ cellText:(NSString*)cellText
+ cellImage:(NSImage*)cellImage;
+
+- (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
+// there is not. If |title| is nil, do not reset the title.
+- (void)setBookmarkCellText:(NSString*)title
+ image:(NSImage*)image;
+
+// Set the color of text in this cell.
+- (void)setTextColor:(NSColor*)color;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BUTTON_CELL_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.mm
new file mode 100644
index 0000000..969a829
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.mm
@@ -0,0 +1,246 @@
+// 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/ui/cocoa/bookmarks/bookmark_button_cell.h"
+
+#include "app/l10n_util_mac.h"
+#include "base/logging.h"
+#include "base/nsimage_cache_mac.h"
+#include "base/sys_string_conversions.h"
+#import "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/metrics/user_metrics.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
+#import "chrome/browser/ui/cocoa/image_utils.h"
+#include "grit/generated_resources.h"
+
+
+@interface BookmarkButtonCell(Private)
+- (void)configureBookmarkButtonCell;
+@end
+
+
+@implementation BookmarkButtonCell
+
+@synthesize startingChildIndex = startingChildIndex_;
+@synthesize drawFolderArrow = drawFolderArrow_;
+
++ (id)buttonCellForNode:(const BookmarkNode*)node
+ contextMenu:(NSMenu*)contextMenu
+ cellText:(NSString*)cellText
+ cellImage:(NSImage*)cellImage {
+ id buttonCell =
+ [[[BookmarkButtonCell alloc] initForNode:node
+ contextMenu:contextMenu
+ cellText:cellText
+ cellImage:cellImage]
+ autorelease];
+ return buttonCell;
+}
+
+- (id)initForNode:(const BookmarkNode*)node
+ contextMenu:(NSMenu*)contextMenu
+ cellText:(NSString*)cellText
+ cellImage:(NSImage*)cellImage {
+ if ((self = [super initTextCell:cellText])) {
+ [self configureBookmarkButtonCell];
+
+ [self setBookmarkNode:node];
+
+ if (node) {
+ NSString* title = base::SysUTF16ToNSString(node->GetTitle());
+ [self setBookmarkCellText:title image:cellImage];
+ [self setMenu:contextMenu];
+ } else {
+ [self setEmpty:YES];
+ [self setBookmarkCellText:l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU)
+ image:nil];
+ }
+ }
+
+ return self;
+}
+
+- (id)initTextCell:(NSString*)string {
+ return [self initForNode:nil contextMenu:nil cellText:string cellImage:nil];
+}
+
+// Used by the off-the-side menu, the only case where a
+// BookmarkButtonCell is loaded from a nib.
+- (void)awakeFromNib {
+ [self configureBookmarkButtonCell];
+}
+
+// Perform all normal init routines specific to the BookmarkButtonCell.
+- (void)configureBookmarkButtonCell {
+ [self setButtonType:NSMomentaryPushInButton];
+ [self setBezelStyle:NSShadowlessSquareBezelStyle];
+ [self setShowsBorderOnlyWhileMouseInside:YES];
+ [self setControlSize:NSSmallControlSize];
+ [self setAlignment:NSLeftTextAlignment];
+ [self setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+ [self setWraps:NO];
+ // NSLineBreakByTruncatingMiddle seems more common on OSX but let's
+ // try to match Windows for a bit to see what happens.
+ [self setLineBreakMode:NSLineBreakByTruncatingTail];
+
+ // Theming doesn't work for bookmark buttons yet (cell text is chucked).
+ [super setShouldTheme:NO];
+}
+
+- (BOOL)empty {
+ return empty_;
+}
+
+- (void)setEmpty:(BOOL)empty {
+ empty_ = empty;
+ [self setShowsBorderOnlyWhileMouseInside:!empty];
+}
+
+- (NSSize)cellSizeForBounds:(NSRect)aRect {
+ NSSize size = [super cellSizeForBounds:aRect];
+ // Cocoa seems to slightly underestimate how much space we need, so we
+ // compensate here to avoid a clipped rendering.
+ size.width += 2;
+ size.height += 4;
+ return size;
+}
+
+- (void)setBookmarkCellText:(NSString*)title
+ image:(NSImage*)image {
+ title = [title stringByReplacingOccurrencesOfString:@"\n"
+ withString:@" "];
+ title = [title stringByReplacingOccurrencesOfString:@"\r"
+ withString:@" "];
+ // If there is no title, squeeze things tight by displaying only the image; by
+ // default, Cocoa leaves extra space in an attempt to display an empty title.
+ if ([title length]) {
+ [self setImagePosition:NSImageLeft];
+ [self setTitle:title];
+ } else {
+ [self setImagePosition:NSImageOnly];
+ }
+
+ if (image)
+ [self setImage:image];
+}
+
+- (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 bookmark node ID to be our represented object.
+- (NSMenu*)menu {
+ if (empty_)
+ return nil;
+ BookmarkMenu* menu = (BookmarkMenu*)[super menu];
+ const BookmarkNode* node =
+ static_cast<const BookmarkNode*>([[self representedObject] pointerValue]);
+
+ if (node->GetParent() && node->GetParent()->type() == BookmarkNode::FOLDER) {
+ UserMetrics::RecordAction(UserMetricsAction("BookmarkBarFolder_CtxMenu"));
+ } else {
+ UserMetrics::RecordAction(UserMetricsAction("BookmarkBar_CtxMenu"));
+ }
+
+ [menu setRepresentedObject:[NSNumber numberWithLongLong:node->id()]];
+
+ return menu;
+}
+
+// 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:NSLeftTextAlignment];
+ NSDictionary* dict = [NSDictionary
+ dictionaryWithObjectsAndKeys:color,
+ NSForegroundColorAttributeName,
+ [self font], NSFontAttributeName,
+ style.get(), NSParagraphStyleAttributeName,
+ nil];
+ scoped_nsobject<NSAttributedString> ats([[NSAttributedString alloc]
+ initWithString:[self title]
+ attributes:dict]);
+ NSButton* button = static_cast<NSButton*>([self controlView]);
+ if (button) {
+ DCHECK([button isKindOfClass:[NSButton class]]);
+ [button setAttributedTitle:ats.get()];
+ }
+}
+
+// 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.
+// Note: as verified in a debugger, mouseEntered: does NOT increase
+// the retainCount of the cell or its owning control.
+- (void)mouseEntered:(NSEvent*)event {
+ [super mouseEntered:event];
+ [[self controlView] mouseEntered:event];
+}
+
+// See comment above mouseEntered:, above.
+- (void)mouseExited:(NSEvent*)event {
+ [[self controlView] mouseExited:event];
+ [super mouseExited:event];
+}
+
+- (void)setDrawFolderArrow:(BOOL)draw {
+ drawFolderArrow_ = draw;
+ if (draw && !arrowImage_) {
+ arrowImage_.reset([nsimage_cache::ImageNamed(@"menu_hierarchy_arrow.pdf")
+ retain]);
+ }
+}
+
+// Add extra size for the arrow so it doesn't overlap the text.
+// Does not sanity check to be sure this is actually a folder node.
+- (NSSize)cellSize {
+ NSSize cellSize = [super cellSize];
+ if (drawFolderArrow_) {
+ cellSize.width += [arrowImage_ size].width; // plus margin?
+ }
+ return cellSize;
+}
+
+// Override cell drawing to add a submenu arrow like a real menu.
+- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
+ // First draw "everything else".
+ [super drawInteriorWithFrame:cellFrame inView:controlView];
+
+ // If asked to do so, and if a folder, draw the arrow.
+ if (!drawFolderArrow_)
+ return;
+ BookmarkButton* button = static_cast<BookmarkButton*>([self controlView]);
+ DCHECK([button respondsToSelector:@selector(isFolder)]);
+ if ([button isFolder]) {
+ NSRect imageRect = NSZeroRect;
+ imageRect.size = [arrowImage_ size];
+ NSRect drawRect = NSOffsetRect(imageRect,
+ NSWidth(cellFrame) - NSWidth(imageRect),
+ (NSHeight(cellFrame) / 2.0) -
+ (NSHeight(imageRect) / 2.0));
+ [arrowImage_ drawInRect:drawRect
+ fromRect:imageRect
+ operation:NSCompositeSourceOver
+ fraction:[self isEnabled] ? 1.0 : 0.5
+ neverFlipped:YES];
+ }
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell_unittest.mm
new file mode 100644
index 0000000..ff26512
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell_unittest.mm
@@ -0,0 +1,183 @@
+// 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 "app/resource_bundle.h"
+#include "base/scoped_nsobject.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "grit/app_resources.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 {
+ public:
+ BrowserTestHelper helper_;
+};
+
+// Make sure it's not totally bogus
+TEST_F(BookmarkButtonCellTest, SizeForBounds) {
+ NSRect frame = NSMakeRect(0, 0, 50, 30);
+ scoped_nsobject<NSButton> view([[NSButton alloc] initWithFrame:frame]);
+ scoped_nsobject<BookmarkButtonCell> cell(
+ [[BookmarkButtonCell alloc] initTextCell:@"Testing"]);
+ [view setCell:cell.get()];
+ [[test_window() contentView] addSubview:view];
+
+ NSRect r = NSMakeRect(0, 0, 100, 100);
+ NSSize size = [cell.get() cellSizeForBounds:r];
+ EXPECT_TRUE(size.width > 0 && size.height > 0);
+ EXPECT_TRUE(size.width < 200 && size.height < 200);
+}
+
+// Make sure icon-only buttons are squeezed tightly.
+TEST_F(BookmarkButtonCellTest, IconOnlySqueeze) {
+ NSRect frame = NSMakeRect(0, 0, 50, 30);
+ scoped_nsobject<NSButton> view([[NSButton alloc] initWithFrame:frame]);
+ scoped_nsobject<BookmarkButtonCell> cell(
+ [[BookmarkButtonCell alloc] initTextCell:@"Testing"]);
+ [view setCell:cell.get()];
+ [[test_window() contentView] addSubview:view];
+
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ scoped_nsobject<NSImage> image([rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON)
+ retain]);
+ EXPECT_TRUE(image.get());
+
+ NSRect r = NSMakeRect(0, 0, 100, 100);
+ [cell setBookmarkCellText:@" " image:image];
+ CGFloat two_space_width = [cell.get() cellSizeForBounds:r].width;
+ [cell setBookmarkCellText:@" " image:image];
+ CGFloat one_space_width = [cell.get() cellSizeForBounds:r].width;
+ [cell setBookmarkCellText:@"" image:image];
+ CGFloat zero_space_width = [cell.get() cellSizeForBounds:r].width;
+
+ // Make sure the switch to "no title" is more significant than we
+ // would otherwise see by decreasing the length of the title.
+ CGFloat delta1 = two_space_width - one_space_width;
+ CGFloat delta2 = one_space_width - zero_space_width;
+ EXPECT_GT(delta2, delta1);
+
+}
+
+// Make sure the default from the base class is overridden.
+TEST_F(BookmarkButtonCellTest, MouseEnterStuff) {
+ scoped_nsobject<BookmarkButtonCell> cell(
+ [[BookmarkButtonCell alloc] initTextCell:@"Testing"]);
+ // Setting the menu should have no affect since we either share or
+ // dynamically compose the menu given a node.
+ [cell setMenu:[[[BookmarkMenu alloc] initWithTitle:@"foo"] autorelease]];
+ EXPECT_FALSE([cell menu]);
+
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* node = model->GetBookmarkBarNode();
+ [cell setEmpty:NO];
+ [cell setBookmarkNode:node];
+ EXPECT_TRUE([cell showsBorderOnlyWhileMouseInside]);
+ EXPECT_TRUE([cell menu]);
+
+ [cell setEmpty:YES];
+ EXPECT_FALSE([cell.get() showsBorderOnlyWhileMouseInside]);
+ EXPECT_FALSE([cell menu]);
+}
+
+TEST_F(BookmarkButtonCellTest, BookmarkNode) {
+ 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);
+}
+
+// Confirms a cell created in a nib is initialized properly
+TEST_F(BookmarkButtonCellTest, Awake) {
+ scoped_nsobject<BookmarkButtonCell> cell([[BookmarkButtonCell alloc] init]);
+ [cell awakeFromNib];
+ EXPECT_EQ(NSLeftTextAlignment, [cell alignment]);
+}
+
+// Subfolder arrow details.
+TEST_F(BookmarkButtonCellTest, FolderArrow) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* bar = model->GetBookmarkBarNode();
+ const BookmarkNode* node = model->AddURL(bar, bar->GetChildCount(),
+ ASCIIToUTF16("title"),
+ GURL("http://www.google.com"));
+ scoped_nsobject<BookmarkButtonCell> cell(
+ [[BookmarkButtonCell alloc] initForNode:node
+ contextMenu:nil
+ cellText:@"small"
+ cellImage:nil]);
+ EXPECT_TRUE(cell.get());
+
+ NSSize size = [cell cellSize];
+ // sanity check
+ EXPECT_GE(size.width, 2);
+ EXPECT_GE(size.height, 2);
+
+ // Once we turn on arrow drawing make sure there is now room for it.
+ [cell setDrawFolderArrow:YES];
+ NSSize arrowSize = [cell cellSize];
+ EXPECT_GT(arrowSize.width, size.width);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_button_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_button_unittest.mm
new file mode 100644
index 0000000..93bd769
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_button_unittest.mm
@@ -0,0 +1,174 @@
+// 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"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h"
+#import "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/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_;
+ BOOL canDragToTrash_;
+ int didDragToTrashCount_;
+}
+@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_++;
+}
+
+- (BOOL)dragShouldLockBarVisibility {
+ return NO;
+}
+
+- (NSWindow*)browserWindow {
+ return nil;
+}
+
+- (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button {
+ return canDragToTrash_;
+}
+
+- (void)didDragBookmarkToTrash:(BookmarkButton*)button {
+ didDragToTrashCount_++;
+}
+
+@end
+
+namespace {
+
+class BookmarkButtonTest : public CocoaTest {
+};
+
+// Make sure nothing leaks
+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, ASCIIToUTF16("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_);
+}
+
+TEST_F(BookmarkButtonTest, DragToTrash) {
+ BrowserTestHelper helper_;
+
+ 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];
+
+ // Add a deletable bookmark to the button.
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* barNode = model->GetBookmarkBarNode();
+ const BookmarkNode* node = model->AddURL(barNode, 0, ASCIIToUTF16("hi mom"),
+ GURL("http://www.google.com"));
+ [cell setBookmarkNode:node];
+
+ // Verify that if canDragBookmarkButtonToTrash is NO then the button can't
+ // be dragged to the trash.
+ delegate.get()->canDragToTrash_ = NO;
+ NSDragOperation operation = [button draggingSourceOperationMaskForLocal:NO];
+ EXPECT_EQ(0u, operation & NSDragOperationDelete);
+ operation = [button draggingSourceOperationMaskForLocal:YES];
+ EXPECT_EQ(0u, operation & NSDragOperationDelete);
+
+ // Verify that if canDragBookmarkButtonToTrash is YES then the button can
+ // be dragged to the trash.
+ delegate.get()->canDragToTrash_ = YES;
+ operation = [button draggingSourceOperationMaskForLocal:NO];
+ EXPECT_EQ(NSDragOperationDelete, operation & NSDragOperationDelete);
+ operation = [button draggingSourceOperationMaskForLocal:YES];
+ EXPECT_EQ(NSDragOperationDelete, operation & NSDragOperationDelete);
+
+ // Verify that canDragBookmarkButtonToTrash is called when expected.
+ delegate.get()->canDragToTrash_ = YES;
+ EXPECT_EQ(0, delegate.get()->didDragToTrashCount_);
+ [button draggedImage:nil endedAt:NSZeroPoint operation:NSDragOperationCopy];
+ EXPECT_EQ(0, delegate.get()->didDragToTrashCount_);
+ [button draggedImage:nil endedAt:NSZeroPoint operation:NSDragOperationMove];
+ EXPECT_EQ(0, delegate.get()->didDragToTrashCount_);
+ [button draggedImage:nil endedAt:NSZeroPoint operation:NSDragOperationDelete];
+ EXPECT_EQ(1, delegate.get()->didDragToTrashCount_);
+}
+
+}
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.h
new file mode 100644
index 0000000..53d4361
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.h
@@ -0,0 +1,30 @@
+// 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 "chrome/browser/bookmarks/bookmark_node_data.h"
+#include "chrome/browser/ui/cocoa/web_contents_drag_source.h"
+
+// A class that handles tracking and event processing for a drag and drop
+// originating from the content area.
+@interface BookmarkDragSource : WebContentsDragSource {
+ @private
+ // Our drop data. Should only be initialized once.
+ std::vector<BookmarkNodeData::Element> dropData_;
+
+ Profile* profile_;
+}
+
+// Initialize a DragDataSource object for a drag (originating on the given
+// contentsView and with the given dropData and pboard). Fill the pasteboard
+// with data types appropriate for dropData.
+- (id)initWithContentsView:(TabContentsViewCocoa*)contentsView
+ dropData:
+ (const std::vector<BookmarkNodeData::Element>&)dropData
+ profile:(Profile*)profile
+ pasteboard:(NSPasteboard*)pboard
+ dragOperationMask:(NSDragOperation)dragOperationMask;
+
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.mm
new file mode 100644
index 0000000..64ae1d9
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.mm
@@ -0,0 +1,43 @@
+// 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/ui/cocoa/bookmarks/bookmark_drag_source.h"
+
+#include "chrome/browser/bookmarks/bookmark_pasteboard_helper_mac.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/tab_contents/tab_contents_view_mac.h"
+
+@implementation BookmarkDragSource
+
+- (id)initWithContentsView:(TabContentsViewCocoa*)contentsView
+ dropData:
+ (const std::vector<BookmarkNodeData::Element>&)dropData
+ profile:(Profile*)profile
+ pasteboard:(NSPasteboard*)pboard
+ dragOperationMask:(NSDragOperation)dragOperationMask {
+ self = [super initWithContentsView:contentsView
+ pasteboard:pboard
+ dragOperationMask:dragOperationMask];
+ if (self) {
+ dropData_ = dropData;
+ profile_ = profile;
+ }
+
+ return self;
+}
+
+- (void)fillPasteboard {
+ bookmark_pasteboard_helper_mac::WriteToDragClipboard(dropData_,
+ profile_->GetPath().value());
+}
+
+- (NSImage*)dragImage {
+ // TODO(feldstein): Do something better than this. Should have badging
+ // and a single drag image.
+ // http://crbug.com/37264
+ return [NSImage imageNamed:NSImageNameMultipleDocuments];
+}
+
+@end // @implementation BookmarkDragSource
+
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h
new file mode 100644
index 0000000..50e7413
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h
@@ -0,0 +1,171 @@
+// 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_UI_COCOA_BOOKMARKS_BOOKMARK_EDITOR_BASE_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_EDITOR_BASE_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_ptr.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/bookmarks/bookmark_editor.h"
+
+class BookmarkEditorBaseControllerBridge;
+class BookmarkModel;
+@class BookmarkTreeBrowserCell;
+
+// A base controller class for bookmark creation and editing dialogs which
+// present the current bookmark folder structure in a tree view. Do not
+// instantiate this controller directly -- use one of its derived classes.
+// NOTE: If a derived class is intended to be dispatched via the
+// BookmarkEditor::Show static function found in the accompanying
+// implementation, that function will need to be update.
+@interface BookmarkEditorBaseController : NSWindowController {
+ @private
+ IBOutlet NSButton* newFolderButton_;
+ IBOutlet NSButton* okButton_; // Used for unit testing only.
+ IBOutlet NSTreeController* folderTreeController_;
+ IBOutlet NSOutlineView* folderTreeView_;
+
+ NSWindow* parentWindow_; // weak
+ Profile* profile_; // weak
+ const BookmarkNode* parentNode_; // weak; owned by the model
+ BookmarkEditor::Configuration configuration_;
+ NSString* initialName_;
+ NSString* displayName_; // Bound to a text field in the dialog.
+ BOOL okEnabled_; // Bound to the OK button.
+ // An array of BookmarkFolderInfo where each item describes a folder in the
+ // BookmarkNode structure.
+ scoped_nsobject<NSArray> folderTreeArray_;
+ // Bound to the table view giving a path to the current selections, of which
+ // there should only ever be one.
+ scoped_nsobject<NSArray> tableSelectionPaths_;
+ // C++ bridge object that observes the BookmarkModel for me.
+ scoped_ptr<BookmarkEditorBaseControllerBridge> observer_;
+}
+
+@property (nonatomic, copy) NSString* initialName;
+@property (nonatomic, copy) NSString* displayName;
+@property (nonatomic, assign) BOOL okEnabled;
+@property (nonatomic, retain, readonly) NSArray* folderTreeArray;
+@property (nonatomic, copy) NSArray* tableSelectionPaths;
+
+// Designated initializer. Derived classes should call through to this init.
+- (id)initWithParentWindow:(NSWindow*)parentWindow
+ nibName:(NSString*)nibName
+ profile:(Profile*)profile
+ parent:(const BookmarkNode*)parent
+ configuration:(BookmarkEditor::Configuration)configuration;
+
+// Run the bookmark editor as a modal sheet. Does not block.
+- (void)runAsModalSheet;
+
+// Create a new folder at the end of the selected parent folder, give it
+// an untitled name, and put it into editing mode.
+- (IBAction)newFolder:(id)sender;
+
+// The cancel action will dismiss the dialog. Derived classes which
+// override cancel:, must call this after accessing any dialog-related
+// data.
+- (IBAction)cancel:(id)sender;
+
+// The OK action will dismiss the dialog. This action is bound
+// to the OK button of a dialog which presents a tree view of a profile's
+// folder hierarchy and allows the creation of new folders within that tree.
+// When the OK button is pressed, this function will: 1) call the derived
+// class's -[willCommit] function, 2) create any new folders created by
+// the user while the dialog is presented, 3) call the derived class's
+// -[didCommit] function, and then 4) dismiss the dialog. At least one
+// of -[willCommit] and -[didCommit] must be provided by the derived class
+// and should return a NSNumber containing a BOOL or nil ('nil' means YES)
+// indicating if the operation should be allowed to continue.
+// Note: A derived class should not override the ok: action.
+- (IBAction)ok:(id)sender;
+
+// Methods for use by derived classes only.
+
+// Determine and returns the rightmost selected/highlighted element (node)
+// in the bookmark tree view if the tree view is showing, otherwise returns
+// the original |parentNode_|. If the tree view is showing but nothing is
+// selected then the root node is returned.
+- (const BookmarkNode*)selectedNode;
+
+// Select/highlight the given node within the browser tree view. If the
+// node is nil then select the bookmark bar node. Exposed for unit test.
+- (void)selectNodeInBrowser:(const BookmarkNode*)node;
+
+// Notifications called when the BookmarkModel changes out from under me.
+- (void)nodeRemoved:(const BookmarkNode*)node
+ fromParent:(const BookmarkNode*)parent;
+- (void)modelChangedPreserveSelection:(BOOL)preserve;
+
+// Accessors
+- (BookmarkModel*)bookmarkModel;
+- (const BookmarkNode*)parentNode;
+
+@end
+
+// Describes the profile's bookmark folder structure: the folder name, the
+// original BookmarkNode pointer (if the folder already exists), a BOOL
+// indicating if the folder is new (meaning: created during this session
+// but not yet committed to the bookmark structure), and an NSArray of
+// child folder BookmarkFolderInfo's following this same structure.
+@interface BookmarkFolderInfo : NSObject {
+ @private
+ NSString* folderName_;
+ const BookmarkNode* folderNode_; // weak
+ NSMutableArray* children_;
+ BOOL newFolder_;
+}
+
+@property (nonatomic, copy) NSString* folderName;
+@property (nonatomic, assign) const BookmarkNode* folderNode;
+@property (nonatomic, retain) NSMutableArray* children;
+@property (nonatomic, assign) BOOL newFolder;
+
+// Convenience creator for adding a new folder to the editor's bookmark
+// structure. This folder will be added to the bookmark model when the
+// user accepts the dialog. |folderName| must be provided.
++ (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName;
+
+// Designated initializer. |folderName| must be provided. For folders which
+// already exist in the bookmark model, |folderNode| and |children| (if any
+// children are already attached to this folder) must be provided and
+// |newFolder| should be NO. For folders which the user has added during
+// this session and which have not been committed yet, |newFolder| should be
+// YES and |folderNode| and |children| should be NULL/nil.
+- (id)initWithFolderName:(NSString*)folderName
+ folderNode:(const BookmarkNode*)folderNode
+ children:(NSMutableArray*)children
+ newFolder:(BOOL)newFolder;
+
+// Convenience creator used during construction of the editor's bookmark
+// structure. |folderName| and |folderNode| must be provided. |children|
+// is optional. Private: exposed here for unit testing purposes.
++ (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName
+ folderNode:(const BookmarkNode*)folderNode
+ children:(NSMutableArray*)children;
+
+@end
+
+@interface BookmarkEditorBaseController(TestingAPI)
+
+@property (nonatomic, readonly) BOOL okButtonEnabled;
+
+// Create any newly added folders. New folders are nodes in folderTreeArray
+// which are marked as being new (i.e. their kFolderTreeNewFolderKey
+// dictionary item is YES). This is called by -[ok:].
+- (void)createNewFolders;
+
+// Select the given bookmark node within the tree view.
+- (void)selectTestNodeInBrowser:(const BookmarkNode*)node;
+
+// Return the dictionary for the folder selected in the tree.
+- (BookmarkFolderInfo*)selectedFolder;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_EDITOR_BASE_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.mm
new file mode 100644
index 0000000..14aa78e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.mm
@@ -0,0 +1,604 @@
+// 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 <stack>
+
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h"
+
+#include "app/l10n_util.h"
+#include "app/l10n_util_mac.h"
+#include "base/logging.h"
+#include "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/profile.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#include "grit/generated_resources.h"
+
+@interface BookmarkEditorBaseController ()
+
+// Return the folder tree object for the given path.
+- (BookmarkFolderInfo*)folderForIndexPath:(NSIndexPath*)path;
+
+// (Re)build the folder tree from the BookmarkModel's current state.
+- (void)buildFolderTree;
+
+// Notifies the controller that the bookmark model has changed.
+// |selection| specifies if the current selection should be
+// maintained (usually YES).
+- (void)modelChangedPreserveSelection:(BOOL)preserve;
+
+// Notifies the controller that a node has been removed.
+- (void)nodeRemoved:(const BookmarkNode*)node
+ fromParent:(const BookmarkNode*)parent;
+
+// Given a folder node, collect an array containing BookmarkFolderInfos
+// describing its subchildren which are also folders.
+- (NSMutableArray*)addChildFoldersFromNode:(const BookmarkNode*)node;
+
+// Scan the folder tree stemming from the given tree folder and create
+// any newly added folders. Pass down info for the folder which was
+// selected before we began creating folders.
+- (void)createNewFoldersForFolder:(BookmarkFolderInfo*)treeFolder
+ selectedFolderInfo:(BookmarkFolderInfo*)selectedFolderInfo;
+
+// Scan the folder tree looking for the given bookmark node and return
+// the selection path thereto.
+- (NSIndexPath*)selectionPathForNode:(const BookmarkNode*)node;
+
+@end
+
+// static; implemented for each platform. Update this function for new
+// classes derived from BookmarkEditorBaseController.
+void BookmarkEditor::Show(gfx::NativeWindow parent_hwnd,
+ Profile* profile,
+ const BookmarkNode* parent,
+ const EditDetails& details,
+ Configuration configuration) {
+ BookmarkEditorBaseController* controller = nil;
+ if (details.type == EditDetails::NEW_FOLDER) {
+ controller = [[BookmarkAllTabsController alloc]
+ initWithParentWindow:parent_hwnd
+ profile:profile
+ parent:parent
+ configuration:configuration];
+ } else {
+ controller = [[BookmarkEditorController alloc]
+ initWithParentWindow:parent_hwnd
+ profile:profile
+ parent:parent
+ node:details.existing_node
+ configuration:configuration];
+ }
+ [controller runAsModalSheet];
+}
+
+// Adapter to tell BookmarkEditorBaseController when bookmarks change.
+class BookmarkEditorBaseControllerBridge : public BookmarkModelObserver {
+ public:
+ BookmarkEditorBaseControllerBridge(BookmarkEditorBaseController* controller)
+ : controller_(controller),
+ importing_(false)
+ { }
+
+ virtual void Loaded(BookmarkModel* model) {
+ [controller_ modelChangedPreserveSelection:YES];
+ }
+
+ virtual void BookmarkNodeMoved(BookmarkModel* model,
+ const BookmarkNode* old_parent,
+ int old_index,
+ const BookmarkNode* new_parent,
+ int new_index) {
+ if (!importing_ && new_parent->GetChild(new_index)->is_folder())
+ [controller_ modelChangedPreserveSelection:YES];
+ }
+
+ virtual void BookmarkNodeAdded(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int index) {
+ if (!importing_ && parent->GetChild(index)->is_folder())
+ [controller_ modelChangedPreserveSelection:YES];
+ }
+
+ virtual void BookmarkNodeRemoved(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int old_index,
+ const BookmarkNode* node) {
+ [controller_ nodeRemoved:node fromParent:parent];
+ if (node->is_folder())
+ [controller_ modelChangedPreserveSelection:NO];
+ }
+
+ virtual void BookmarkNodeChanged(BookmarkModel* model,
+ const BookmarkNode* node) {
+ if (!importing_ && node->is_folder())
+ [controller_ modelChangedPreserveSelection:YES];
+ }
+
+ virtual void BookmarkNodeChildrenReordered(BookmarkModel* model,
+ const BookmarkNode* node) {
+ if (!importing_)
+ [controller_ modelChangedPreserveSelection:YES];
+ }
+
+ virtual void BookmarkNodeFavIconLoaded(BookmarkModel* model,
+ const BookmarkNode* node) {
+ // I care nothing for these 'favicons': I only show folders.
+ }
+
+ virtual void BookmarkImportBeginning(BookmarkModel* model) {
+ importing_ = true;
+ }
+
+ // Invoked after a batch import finishes. This tells observers to update
+ // themselves if they were waiting for the update to finish.
+ virtual void BookmarkImportEnding(BookmarkModel* model) {
+ importing_ = false;
+ [controller_ modelChangedPreserveSelection:YES];
+ }
+
+ private:
+ BookmarkEditorBaseController* controller_; // weak
+ bool importing_;
+};
+
+
+#pragma mark -
+
+@implementation BookmarkEditorBaseController
+
+@synthesize initialName = initialName_;
+@synthesize displayName = displayName_;
+@synthesize okEnabled = okEnabled_;
+
+- (id)initWithParentWindow:(NSWindow*)parentWindow
+ nibName:(NSString*)nibName
+ profile:(Profile*)profile
+ parent:(const BookmarkNode*)parent
+ configuration:(BookmarkEditor::Configuration)configuration {
+ NSString* nibpath = [mac_util::MainAppBundle()
+ pathForResource:nibName
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ parentWindow_ = parentWindow;
+ profile_ = profile;
+ parentNode_ = parent;
+ configuration_ = configuration;
+ initialName_ = [@"" retain];
+ observer_.reset(new BookmarkEditorBaseControllerBridge(self));
+ [self bookmarkModel]->AddObserver(observer_.get());
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [self bookmarkModel]->RemoveObserver(observer_.get());
+ [initialName_ release];
+ [displayName_ release];
+ [super dealloc];
+}
+
+- (void)awakeFromNib {
+ [self setDisplayName:[self initialName]];
+
+ if (configuration_ != BookmarkEditor::SHOW_TREE) {
+ // Remember the tree view's height; we will shrink our frame by that much.
+ NSRect frame = [[self window] frame];
+ CGFloat browserHeight = [folderTreeView_ frame].size.height;
+ frame.size.height -= browserHeight;
+ frame.origin.y += browserHeight;
+ // Remove the folder tree and "new folder" button.
+ [folderTreeView_ removeFromSuperview];
+ [newFolderButton_ removeFromSuperview];
+ // Finally, commit the size change.
+ [[self window] setFrame:frame display:YES];
+ }
+
+ // Build up a tree of the current folder configuration.
+ [self buildFolderTree];
+}
+
+- (void)windowDidLoad {
+ if (configuration_ == BookmarkEditor::SHOW_TREE) {
+ [self selectNodeInBrowser:parentNode_];
+ }
+}
+
+/* TODO(jrg):
+// Implementing this informal protocol allows us to open the sheet
+// somewhere other than at the top of the window. NOTE: this means
+// that I, the controller, am also the window's delegate.
+- (NSRect)window:(NSWindow*)window willPositionSheet:(NSWindow*)sheet
+ usingRect:(NSRect)rect {
+ // adjust rect.origin.y to be the bottom of the toolbar
+ return rect;
+}
+*/
+
+// TODO(jrg): consider NSModalSession.
+- (void)runAsModalSheet {
+ // Lock down floating bar when in full-screen mode. Don't animate
+ // otherwise the pane will be misplaced.
+ [[BrowserWindowController browserWindowControllerForWindow:parentWindow_]
+ lockBarVisibilityForOwner:self withAnimation:NO delay:NO];
+ [NSApp beginSheet:[self window]
+ modalForWindow:parentWindow_
+ modalDelegate:self
+ didEndSelector:@selector(didEndSheet:returnCode:contextInfo:)
+ contextInfo:nil];
+}
+
+- (BOOL)okEnabled {
+ return YES;
+}
+
+- (IBAction)ok:(id)sender {
+ // At least one of these two functions should be provided by derived classes.
+ BOOL hasWillCommit = [self respondsToSelector:@selector(willCommit)];
+ BOOL hasDidCommit = [self respondsToSelector:@selector(didCommit)];
+ DCHECK(hasWillCommit || hasDidCommit);
+ BOOL shouldContinue = YES;
+ if (hasWillCommit) {
+ NSNumber* hasWillContinue = [self performSelector:@selector(willCommit)];
+ if (hasWillContinue && [hasWillContinue isKindOfClass:[NSNumber class]])
+ shouldContinue = [hasWillContinue boolValue];
+ }
+ if (shouldContinue)
+ [self createNewFolders];
+ if (hasDidCommit) {
+ NSNumber* hasDidContinue = [self performSelector:@selector(didCommit)];
+ if (hasDidContinue && [hasDidContinue isKindOfClass:[NSNumber class]])
+ shouldContinue = [hasDidContinue boolValue];
+ }
+ if (shouldContinue)
+ [NSApp endSheet:[self window]];
+}
+
+- (IBAction)cancel:(id)sender {
+ [NSApp endSheet:[self window]];
+}
+
+- (void)didEndSheet:(NSWindow*)sheet
+ returnCode:(int)returnCode
+ contextInfo:(void*)contextInfo {
+ [sheet close];
+ [[BrowserWindowController browserWindowControllerForWindow:parentWindow_]
+ releaseBarVisibilityForOwner:self withAnimation:YES delay:NO];
+}
+
+- (void)windowWillClose:(NSNotification*)notification {
+ [self autorelease];
+}
+
+#pragma mark Folder Tree Management
+
+- (BookmarkModel*)bookmarkModel {
+ return profile_->GetBookmarkModel();
+}
+
+- (const BookmarkNode*)parentNode {
+ return parentNode_;
+}
+
+- (BookmarkFolderInfo*)folderForIndexPath:(NSIndexPath*)indexPath {
+ NSUInteger pathCount = [indexPath length];
+ BookmarkFolderInfo* item = nil;
+ NSArray* treeNode = [self folderTreeArray];
+ for (NSUInteger i = 0; i < pathCount; ++i) {
+ item = [treeNode objectAtIndex:[indexPath indexAtPosition:i]];
+ treeNode = [item children];
+ }
+ return item;
+}
+
+- (NSIndexPath*)selectedIndexPath {
+ NSIndexPath* selectedIndexPath = nil;
+ NSArray* selections = [self tableSelectionPaths];
+ if ([selections count]) {
+ DCHECK([selections count] == 1); // Should be exactly one selection.
+ selectedIndexPath = [selections objectAtIndex:0];
+ }
+ return selectedIndexPath;
+}
+
+- (BookmarkFolderInfo*)selectedFolder {
+ BookmarkFolderInfo* item = nil;
+ NSIndexPath* selectedIndexPath = [self selectedIndexPath];
+ if (selectedIndexPath) {
+ item = [self folderForIndexPath:selectedIndexPath];
+ }
+ return item;
+}
+
+- (const BookmarkNode*)selectedNode {
+ const BookmarkNode* selectedNode = NULL;
+ // Determine a new parent node only if the browser is showing.
+ if (configuration_ == BookmarkEditor::SHOW_TREE) {
+ BookmarkFolderInfo* folderInfo = [self selectedFolder];
+ if (folderInfo)
+ selectedNode = [folderInfo folderNode];
+ } else {
+ // If the tree is not showing then we use the original parent.
+ selectedNode = parentNode_;
+ }
+ return selectedNode;
+}
+
+- (NSArray*)folderTreeArray {
+ return folderTreeArray_.get();
+}
+
+- (NSArray*)tableSelectionPaths {
+ return tableSelectionPaths_.get();
+}
+
+- (void)setTableSelectionPath:(NSIndexPath*)tableSelectionPath {
+ [self setTableSelectionPaths:[NSArray arrayWithObject:tableSelectionPath]];
+}
+
+- (void)setTableSelectionPaths:(NSArray*)tableSelectionPaths {
+ tableSelectionPaths_.reset([tableSelectionPaths retain]);
+}
+
+- (void)selectNodeInBrowser:(const BookmarkNode*)node {
+ DCHECK(configuration_ == BookmarkEditor::SHOW_TREE);
+ NSIndexPath* selectionPath = [self selectionPathForNode:node];
+ [self willChangeValueForKey:@"okEnabled"];
+ [self setTableSelectionPath:selectionPath];
+ [self didChangeValueForKey:@"okEnabled"];
+}
+
+- (NSIndexPath*)selectionPathForNode:(const BookmarkNode*)desiredNode {
+ // Back up the parent chaing for desiredNode, building up a stack
+ // of ancestor nodes. Then crawl down the folderTreeArray looking
+ // for each ancestor in order while building up the selectionPath.
+ std::stack<const BookmarkNode*> nodeStack;
+ BookmarkModel* model = profile_->GetBookmarkModel();
+ const BookmarkNode* rootNode = model->root_node();
+ const BookmarkNode* node = desiredNode;
+ while (node != rootNode) {
+ DCHECK(node);
+ nodeStack.push(node);
+ node = node->GetParent();
+ }
+ NSUInteger stackSize = nodeStack.size();
+
+ NSIndexPath* path = nil;
+ NSArray* folders = [self folderTreeArray];
+ while (!nodeStack.empty()) {
+ node = nodeStack.top();
+ nodeStack.pop();
+ // Find node in the current folders array.
+ NSUInteger i = 0;
+ for (BookmarkFolderInfo *folderInfo in folders) {
+ const BookmarkNode* testNode = [folderInfo folderNode];
+ if (testNode == node) {
+ path = path ? [path indexPathByAddingIndex:i] :
+ [NSIndexPath indexPathWithIndex:i];
+ folders = [folderInfo children];
+ break;
+ }
+ ++i;
+ }
+ }
+ DCHECK([path length] == stackSize);
+ return path;
+}
+
+- (NSMutableArray*)addChildFoldersFromNode:(const BookmarkNode*)node {
+ NSMutableArray* childFolders = nil;
+ int childCount = node->GetChildCount();
+ for (int i = 0; i < childCount; ++i) {
+ const BookmarkNode* childNode = node->GetChild(i);
+ if (childNode->type() != BookmarkNode::URL) {
+ NSString* childName = base::SysUTF16ToNSString(childNode->GetTitle());
+ NSMutableArray* children = [self addChildFoldersFromNode:childNode];
+ BookmarkFolderInfo* folderInfo =
+ [BookmarkFolderInfo bookmarkFolderInfoWithFolderName:childName
+ folderNode:childNode
+ children:children];
+ if (!childFolders)
+ childFolders = [NSMutableArray arrayWithObject:folderInfo];
+ else
+ [childFolders addObject:folderInfo];
+ }
+ }
+ return childFolders;
+}
+
+- (void)buildFolderTree {
+ // Build up a tree of the current folder configuration.
+ BookmarkModel* model = profile_->GetBookmarkModel();
+ const BookmarkNode* rootNode = model->root_node();
+ NSMutableArray* baseArray = [self addChildFoldersFromNode:rootNode];
+ DCHECK(baseArray);
+ [self willChangeValueForKey:@"folderTreeArray"];
+ folderTreeArray_.reset([baseArray retain]);
+ [self didChangeValueForKey:@"folderTreeArray"];
+}
+
+- (void)modelChangedPreserveSelection:(BOOL)preserve {
+ const BookmarkNode* selectedNode = [self selectedNode];
+ [self buildFolderTree];
+ if (preserve &&
+ selectedNode &&
+ configuration_ == BookmarkEditor::SHOW_TREE)
+ [self selectNodeInBrowser:selectedNode];
+}
+
+- (void)nodeRemoved:(const BookmarkNode*)node
+ fromParent:(const BookmarkNode*)parent {
+ if (node->is_folder()) {
+ if (parentNode_ == node || parentNode_->HasAncestor(node)) {
+ parentNode_ = [self bookmarkModel]->GetBookmarkBarNode();
+ if (configuration_ != BookmarkEditor::SHOW_TREE) {
+ // The user can't select a different folder, so just close up shop.
+ [self cancel:self];
+ return;
+ }
+ }
+
+ if (configuration_ == BookmarkEditor::SHOW_TREE) {
+ // For safety's sake, in case deleted node was an ancestor of selection,
+ // go back to a known safe place.
+ [self selectNodeInBrowser:parentNode_];
+ }
+ }
+}
+
+#pragma mark New Folder Handler
+
+- (void)createNewFoldersForFolder:(BookmarkFolderInfo*)folderInfo
+ selectedFolderInfo:(BookmarkFolderInfo*)selectedFolderInfo {
+ NSArray* subfolders = [folderInfo children];
+ const BookmarkNode* parentNode = [folderInfo folderNode];
+ DCHECK(parentNode);
+ NSUInteger i = 0;
+ for (BookmarkFolderInfo* subFolderInfo in subfolders) {
+ if ([subFolderInfo newFolder]) {
+ BookmarkModel* model = [self bookmarkModel];
+ const BookmarkNode* newFolder =
+ model->AddGroup(parentNode, i,
+ base::SysNSStringToUTF16([subFolderInfo folderName]));
+ // Update our dictionary with the actual folder node just created.
+ [subFolderInfo setFolderNode:newFolder];
+ [subFolderInfo setNewFolder:NO];
+ // If the newly created folder was selected, update the selection path.
+ if (subFolderInfo == selectedFolderInfo) {
+ NSIndexPath* selectionPath = [self selectionPathForNode:newFolder];
+ [self setTableSelectionPath:selectionPath];
+ }
+ }
+ [self createNewFoldersForFolder:subFolderInfo
+ selectedFolderInfo:selectedFolderInfo];
+ ++i;
+ }
+}
+
+- (IBAction)newFolder:(id)sender {
+ // Create a new folder off of the selected folder node.
+ BookmarkFolderInfo* parentInfo = [self selectedFolder];
+ if (parentInfo) {
+ NSIndexPath* selection = [self selectedIndexPath];
+ NSString* newFolderName =
+ l10n_util::GetNSStringWithFixup(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME);
+ BookmarkFolderInfo* folderInfo =
+ [BookmarkFolderInfo bookmarkFolderInfoWithFolderName:newFolderName];
+ [self willChangeValueForKey:@"folderTreeArray"];
+ NSMutableArray* children = [parentInfo children];
+ if (children) {
+ [children addObject:folderInfo];
+ } else {
+ children = [NSMutableArray arrayWithObject:folderInfo];
+ [parentInfo setChildren:children];
+ }
+ [self didChangeValueForKey:@"folderTreeArray"];
+
+ // Expose the parent folder children.
+ [folderTreeView_ expandItem:parentInfo];
+
+ // Select the new folder node and put the folder name into edit mode.
+ selection = [selection indexPathByAddingIndex:[children count] - 1];
+ [self setTableSelectionPath:selection];
+ NSInteger row = [folderTreeView_ selectedRow];
+ DCHECK(row >= 0);
+ [folderTreeView_ editColumn:0 row:row withEvent:nil select:YES];
+ }
+}
+
+- (void)createNewFolders {
+ // Turn off notifications while "importing" folders (as created in the sheet).
+ observer_->BookmarkImportBeginning([self bookmarkModel]);
+ // Scan the tree looking for nodes marked 'newFolder' and create those nodes.
+ NSArray* folderTreeArray = [self folderTreeArray];
+ for (BookmarkFolderInfo *folderInfo in folderTreeArray) {
+ [self createNewFoldersForFolder:folderInfo
+ selectedFolderInfo:[self selectedFolder]];
+ }
+ // Notifications back on.
+ observer_->BookmarkImportEnding([self bookmarkModel]);
+}
+
+#pragma mark For Unit Test Use Only
+
+- (BOOL)okButtonEnabled {
+ return [okButton_ isEnabled];
+}
+
+- (void)selectTestNodeInBrowser:(const BookmarkNode*)node {
+ [self selectNodeInBrowser:node];
+}
+
+@end // BookmarkEditorBaseController
+
+@implementation BookmarkFolderInfo
+
+@synthesize folderName = folderName_;
+@synthesize folderNode = folderNode_;
+@synthesize children = children_;
+@synthesize newFolder = newFolder_;
+
++ (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName
+ folderNode:(const BookmarkNode*)folderNode
+ children:(NSMutableArray*)children {
+ return [[[BookmarkFolderInfo alloc] initWithFolderName:folderName
+ folderNode:folderNode
+ children:children
+ newFolder:NO]
+ autorelease];
+}
+
++ (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName {
+ return [[[BookmarkFolderInfo alloc] initWithFolderName:folderName
+ folderNode:NULL
+ children:nil
+ newFolder:YES]
+ autorelease];
+}
+
+- (id)initWithFolderName:(NSString*)folderName
+ folderNode:(const BookmarkNode*)folderNode
+ children:(NSMutableArray*)children
+ newFolder:(BOOL)newFolder {
+ if ((self = [super init])) {
+ // A folderName is always required, and if newFolder is NO then there
+ // should be a folderNode. Children is optional.
+ DCHECK(folderName && (newFolder || folderNode));
+ if (folderName && (newFolder || folderNode)) {
+ folderName_ = [folderName copy];
+ folderNode_ = folderNode;
+ children_ = [children retain];
+ newFolder_ = newFolder;
+ } else {
+ NOTREACHED(); // Invalid init.
+ [self release];
+ self = nil;
+ }
+ }
+ return self;
+}
+
+- (id)init {
+ NOTREACHED(); // Should never be called.
+ return [self initWithFolderName:nil folderNode:nil children:nil newFolder:NO];
+}
+
+- (void)dealloc {
+ [folderName_ release];
+ [children_ release];
+ [super dealloc];
+}
+
+// Implementing isEqual: allows the NSTreeController to preserve the selection
+// and open/shut state of outline items when the data changes.
+- (BOOL)isEqual:(id)other {
+ return [other isKindOfClass:[BookmarkFolderInfo class]] &&
+ folderNode_ == [(BookmarkFolderInfo*)other folderNode];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller_unittest.mm
new file mode 100644
index 0000000..6325346
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller_unittest.mm
@@ -0,0 +1,235 @@
+// 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 "app/l10n_util_mac.h"
+#include "base/scoped_nsobject.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "grit/generated_resources.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+class BookmarkEditorBaseControllerTest : public CocoaTest {
+ public:
+ BrowserTestHelper browser_helper_;
+ BookmarkEditorBaseController* controller_; // weak
+ const BookmarkNode* group_a_;
+ const BookmarkNode* group_b_;
+ const BookmarkNode* group_b_0_;
+ const BookmarkNode* group_b_3_;
+ const BookmarkNode* group_c_;
+
+ BookmarkEditorBaseControllerTest() {
+ // Set up a small bookmark hierarchy, which will look as follows:
+ // a b c d
+ // a-0 b-0 c-0
+ // a-1 b-00 c-1
+ // a-2 b-1 c-2
+ // b-2 c-3
+ // b-3
+ // b-30
+ // b-31
+ // b-4
+ BookmarkModel& model(*(browser_helper_.profile()->GetBookmarkModel()));
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ group_a_ = model.AddGroup(root, 0, ASCIIToUTF16("a"));
+ model.AddURL(group_a_, 0, ASCIIToUTF16("a-0"), GURL("http://a-0.com"));
+ model.AddURL(group_a_, 1, ASCIIToUTF16("a-1"), GURL("http://a-1.com"));
+ model.AddURL(group_a_, 2, ASCIIToUTF16("a-2"), GURL("http://a-2.com"));
+
+ group_b_ = model.AddGroup(root, 1, ASCIIToUTF16("b"));
+ group_b_0_ = model.AddGroup(group_b_, 0, ASCIIToUTF16("b-0"));
+ model.AddURL(group_b_0_, 0, ASCIIToUTF16("bb-0"), GURL("http://bb-0.com"));
+ model.AddURL(group_b_, 1, ASCIIToUTF16("b-1"), GURL("http://b-1.com"));
+ model.AddURL(group_b_, 2, ASCIIToUTF16("b-2"), GURL("http://b-2.com"));
+ group_b_3_ = model.AddGroup(group_b_, 3, ASCIIToUTF16("b-3"));
+ model.AddURL(group_b_3_, 0, ASCIIToUTF16("b-30"), GURL("http://b-30.com"));
+ model.AddURL(group_b_3_, 1, ASCIIToUTF16("b-31"), GURL("http://b-31.com"));
+ model.AddURL(group_b_, 4, ASCIIToUTF16("b-4"), GURL("http://b-4.com"));
+
+ group_c_ = model.AddGroup(root, 2, ASCIIToUTF16("c"));
+ model.AddURL(group_c_, 0, ASCIIToUTF16("c-0"), GURL("http://c-0.com"));
+ model.AddURL(group_c_, 1, ASCIIToUTF16("c-1"), GURL("http://c-1.com"));
+ model.AddURL(group_c_, 2, ASCIIToUTF16("c-2"), GURL("http://c-2.com"));
+ model.AddURL(group_c_, 3, ASCIIToUTF16("c-3"), GURL("http://c-3.com"));
+
+ model.AddURL(root, 3, ASCIIToUTF16("d"), GURL("http://d-0.com"));
+ }
+
+ virtual BookmarkEditorBaseController* CreateController() {
+ return [[BookmarkEditorBaseController alloc]
+ initWithParentWindow:test_window()
+ nibName:@"BookmarkAllTabs"
+ profile:browser_helper_.profile()
+ parent:group_b_0_
+ configuration:BookmarkEditor::SHOW_TREE];
+ }
+
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ controller_ = CreateController();
+ EXPECT_TRUE([controller_ window]);
+ [controller_ runAsModalSheet];
+ }
+
+ virtual void TearDown() {
+ controller_ = NULL;
+ CocoaTest::TearDown();
+ }
+};
+
+TEST_F(BookmarkEditorBaseControllerTest, VerifyBookmarkTestModel) {
+ BookmarkModel& model(*(browser_helper_.profile()->GetBookmarkModel()));
+ const BookmarkNode& root(*model.GetBookmarkBarNode());
+ EXPECT_EQ(4, root.GetChildCount());
+ // a
+ const BookmarkNode* child = root.GetChild(0);
+ EXPECT_EQ(3, child->GetChildCount());
+ const BookmarkNode* subchild = child->GetChild(0);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ subchild = child->GetChild(1);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ subchild = child->GetChild(2);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ // b
+ child = root.GetChild(1);
+ EXPECT_EQ(5, child->GetChildCount());
+ subchild = child->GetChild(0);
+ EXPECT_EQ(1, subchild->GetChildCount());
+ const BookmarkNode* subsubchild = subchild->GetChild(0);
+ EXPECT_EQ(0, subsubchild->GetChildCount());
+ subchild = child->GetChild(1);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ subchild = child->GetChild(2);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ subchild = child->GetChild(3);
+ EXPECT_EQ(2, subchild->GetChildCount());
+ subsubchild = subchild->GetChild(0);
+ EXPECT_EQ(0, subsubchild->GetChildCount());
+ subsubchild = subchild->GetChild(1);
+ EXPECT_EQ(0, subsubchild->GetChildCount());
+ subchild = child->GetChild(4);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ // c
+ child = root.GetChild(2);
+ EXPECT_EQ(4, child->GetChildCount());
+ subchild = child->GetChild(0);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ subchild = child->GetChild(1);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ subchild = child->GetChild(2);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ subchild = child->GetChild(3);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ // d
+ child = root.GetChild(3);
+ EXPECT_EQ(0, child->GetChildCount());
+ [controller_ cancel:nil];
+}
+
+TEST_F(BookmarkEditorBaseControllerTest, NodeSelection) {
+ EXPECT_TRUE([controller_ folderTreeArray]);
+ [controller_ selectTestNodeInBrowser:group_b_3_];
+ const BookmarkNode* node = [controller_ selectedNode];
+ EXPECT_EQ(node, group_b_3_);
+ [controller_ cancel:nil];
+}
+
+TEST_F(BookmarkEditorBaseControllerTest, CreateFolder) {
+ EXPECT_EQ(2, group_b_3_->GetChildCount());
+ [controller_ selectTestNodeInBrowser:group_b_3_];
+ NSString* expectedName =
+ l10n_util::GetNSStringWithFixup(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME);
+ [controller_ setDisplayName:expectedName];
+ [controller_ newFolder:nil];
+ NSArray* selectionPaths = [controller_ tableSelectionPaths];
+ EXPECT_EQ(1U, [selectionPaths count]);
+ NSIndexPath* selectionPath = [selectionPaths objectAtIndex:0];
+ EXPECT_EQ(4U, [selectionPath length]);
+ BookmarkFolderInfo* newFolderInfo = [controller_ selectedFolder];
+ EXPECT_TRUE(newFolderInfo);
+ NSString* newFolderName = [newFolderInfo folderName];
+ EXPECT_NSEQ(expectedName, newFolderName);
+ [controller_ createNewFolders];
+ // Verify that the tab folder was added to the new folder.
+ EXPECT_EQ(3, group_b_3_->GetChildCount());
+ [controller_ cancel:nil];
+}
+
+TEST_F(BookmarkEditorBaseControllerTest, CreateTwoFolders) {
+ BookmarkModel* model = browser_helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* bar = model->GetBookmarkBarNode();
+ // Create 2 folders which are children of the bar.
+ [controller_ selectTestNodeInBrowser:bar];
+ [controller_ newFolder:nil];
+ [controller_ selectTestNodeInBrowser:bar];
+ [controller_ newFolder:nil];
+ // If we do NOT crash on createNewFolders, success!
+ // (e.g. http://crbug.com/47877 is fixed).
+ [controller_ createNewFolders];
+ [controller_ cancel:nil];
+}
+
+TEST_F(BookmarkEditorBaseControllerTest, SelectedFolderDeleted) {
+ BookmarkModel& model(*(browser_helper_.profile()->GetBookmarkModel()));
+ [controller_ selectTestNodeInBrowser:group_b_3_];
+ EXPECT_EQ(group_b_3_, [controller_ selectedNode]);
+
+ // Delete the selected node, and verify it's no longer selected:
+ model.Remove(group_b_, 3);
+ EXPECT_NE(group_b_3_, [controller_ selectedNode]);
+
+ [controller_ cancel:nil];
+}
+
+TEST_F(BookmarkEditorBaseControllerTest, SelectedFoldersParentDeleted) {
+ BookmarkModel& model(*(browser_helper_.profile()->GetBookmarkModel()));
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ [controller_ selectTestNodeInBrowser:group_b_3_];
+ EXPECT_EQ(group_b_3_, [controller_ selectedNode]);
+
+ // Delete the selected node's parent, and verify it's no longer selected:
+ model.Remove(root, 1);
+ EXPECT_NE(group_b_3_, [controller_ selectedNode]);
+
+ [controller_ cancel:nil];
+}
+
+TEST_F(BookmarkEditorBaseControllerTest, FolderAdded) {
+ BookmarkModel& model(*(browser_helper_.profile()->GetBookmarkModel()));
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+
+ // Add a group node to the model, and verify it can be selected in the tree:
+ const BookmarkNode* group_added = model.AddGroup(root, 0,
+ ASCIIToUTF16("added"));
+ [controller_ selectTestNodeInBrowser:group_added];
+ EXPECT_EQ(group_added, [controller_ selectedNode]);
+
+ [controller_ cancel:nil];
+}
+
+
+class BookmarkFolderInfoTest : public CocoaTest { };
+
+TEST_F(BookmarkFolderInfoTest, Construction) {
+ NSMutableArray* children = [NSMutableArray arrayWithObject:@"child"];
+ // We just need a pointer, and any pointer will do.
+ const BookmarkNode* fakeNode =
+ reinterpret_cast<const BookmarkNode*>(&children);
+ BookmarkFolderInfo* info =
+ [BookmarkFolderInfo bookmarkFolderInfoWithFolderName:@"name"
+ folderNode:fakeNode
+ children:children];
+ EXPECT_TRUE(info);
+ EXPECT_EQ([info folderName], @"name");
+ EXPECT_EQ([info children], children);
+ EXPECT_EQ([info folderNode], fakeNode);
+}
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h
new file mode 100644
index 0000000..30f1c75
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h
@@ -0,0 +1,36 @@
+// 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_UI_COCOA_BOOKMARKS_BOOKMARK_EDITOR_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_EDITOR_CONTROLLER_H_
+#pragma once
+
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h"
+
+// A controller for the bookmark editor, opened by 1) Edit... from the
+// context menu of a bookmark button, and 2) Bookmark this Page...'s Edit
+// button.
+@interface BookmarkEditorController : BookmarkEditorBaseController {
+ @private
+ const BookmarkNode* node_; // weak; owned by the model
+ scoped_nsobject<NSString> initialUrl_;
+ NSString* displayURL_; // Bound to a text field in the dialog.
+ IBOutlet NSTextField* urlField_;
+}
+
+@property (nonatomic, copy) NSString* displayURL;
+
+- (id)initWithParentWindow:(NSWindow*)parentWindow
+ profile:(Profile*)profile
+ parent:(const BookmarkNode*)parent
+ node:(const BookmarkNode*)node
+ configuration:(BookmarkEditor::Configuration)configuration;
+
+@end
+
+@interface BookmarkEditorController (UnitTesting)
+- (NSColor *)urlFieldColor;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_EDITOR_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.mm
new file mode 100644
index 0000000..88ed4bf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.mm
@@ -0,0 +1,143 @@
+// 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/ui/cocoa/bookmarks/bookmark_editor_controller.h"
+
+#include "app/l10n_util.h"
+#include "base/string16.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+
+@interface BookmarkEditorController (Private)
+
+// Grab the url from the text field and convert.
+- (GURL)GURLFromUrlField;
+
+@end
+
+@implementation BookmarkEditorController
+
+@synthesize displayURL = displayURL_;
+
++ (NSSet*)keyPathsForValuesAffectingOkEnabled {
+ return [NSSet setWithObject:@"displayURL"];
+}
+
+- (id)initWithParentWindow:(NSWindow*)parentWindow
+ profile:(Profile*)profile
+ parent:(const BookmarkNode*)parent
+ node:(const BookmarkNode*)node
+ configuration:(BookmarkEditor::Configuration)configuration {
+ if ((self = [super initWithParentWindow:parentWindow
+ nibName:@"BookmarkEditor"
+ profile:profile
+ parent:parent
+ configuration:configuration])) {
+ // "Add Page..." has no "node" so this may be NULL.
+ node_ = node;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [displayURL_ release];
+ [super dealloc];
+}
+
+- (void)awakeFromNib {
+ // Set text fields to match our bookmark. If the node is NULL we
+ // arrived here from an "Add Page..." item in a context menu.
+ if (node_) {
+ [self setInitialName:base::SysUTF16ToNSString(node_->GetTitle())];
+ std::string url_string = node_->GetURL().possibly_invalid_spec();
+ initialUrl_.reset([[NSString stringWithUTF8String:url_string.c_str()]
+ retain]);
+ } else {
+ initialUrl_.reset([@"" retain]);
+ }
+ [self setDisplayURL:initialUrl_];
+ [super awakeFromNib];
+}
+
+- (void)nodeRemoved:(const BookmarkNode*)node
+ fromParent:(const BookmarkNode*)parent
+{
+ // Be conservative; it is needed (e.g. "Add Page...")
+ node_ = NULL;
+ [self cancel:self];
+}
+
+#pragma mark Bookmark Editing
+
+// If possible, return a valid GURL from the URL text field.
+- (GURL)GURLFromUrlField {
+ NSString* url = [self displayURL];
+ GURL newURL = GURL([url UTF8String]);
+ if (!newURL.is_valid()) {
+ // Mimic observed friendliness from Windows
+ newURL = GURL([[NSString stringWithFormat:@"http://%@", url] UTF8String]);
+ }
+ return newURL;
+}
+
+// Enable the OK button if there is a valid URL.
+- (BOOL)okEnabled {
+ BOOL okEnabled = NO;
+ if ([[self displayURL] length]) {
+ GURL newURL = [self GURLFromUrlField];
+ okEnabled = (newURL.is_valid()) ? YES : NO;
+ }
+ if (okEnabled)
+ [urlField_ setBackgroundColor:[NSColor whiteColor]];
+ else
+ [urlField_ setBackgroundColor:[NSColor colorWithCalibratedRed:1.0
+ green:0.67
+ blue:0.67
+ alpha:1.0]];
+ return okEnabled;
+}
+
+// The the bookmark's URL is assumed to be valid (otherwise the OK button
+// should not be enabled). Previously existing bookmarks for which the
+// parent has not changed are updated in-place. Those for which the parent
+// has changed are removed with a new node created under the new parent.
+// Called by -[BookmarkEditorBaseController ok:].
+- (NSNumber*)didCommit {
+ NSString* name = [[self displayName] stringByTrimmingCharactersInSet:
+ [NSCharacterSet newlineCharacterSet]];
+ string16 newTitle = base::SysNSStringToUTF16(name);
+ const BookmarkNode* newParentNode = [self selectedNode];
+ GURL newURL = [self GURLFromUrlField];
+ if (!newURL.is_valid()) {
+ // Shouldn't be reached -- OK button should be disabled if not valid!
+ NOTREACHED();
+ return [NSNumber numberWithBool:NO];
+ }
+
+ // Determine where the new/replacement bookmark is to go.
+ BookmarkModel* model = [self bookmarkModel];
+ // If there was an old node then we update the node, and move it to its new
+ // parent if the parent has changed (rather than deleting it from the old
+ // parent and adding to the new -- which also prevents the 'poofing' that
+ // occurs when a node is deleted).
+ if (node_) {
+ model->SetURL(node_, newURL);
+ model->SetTitle(node_, newTitle);
+ const BookmarkNode* oldParentNode = [self parentNode];
+ if (newParentNode != oldParentNode)
+ model->Move(node_, newParentNode, newParentNode->GetChildCount());
+ } else {
+ // Otherwise, add a new bookmark at the end of the newly selected folder.
+ model->AddURL(newParentNode, newParentNode->GetChildCount(), newTitle,
+ newURL);
+ }
+ return [NSNumber numberWithBool:YES];
+}
+
+- (NSColor *)urlFieldColor {
+ return [urlField_ backgroundColor];
+}
+
+@end // BookmarkEditorController
+
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller_unittest.mm
new file mode 100644
index 0000000..8f49c6d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller_unittest.mm
@@ -0,0 +1,423 @@
+// 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/string16.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+class BookmarkEditorControllerTest : public CocoaTest {
+ public:
+ BrowserTestHelper browser_helper_;
+ const BookmarkNode* default_node_;
+ const BookmarkNode* default_parent_;
+ const char* default_name_;
+ string16 default_title_;
+ BookmarkEditorController* controller_;
+
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ BookmarkModel* model = browser_helper_.profile()->GetBookmarkModel();
+ default_parent_ = model->GetBookmarkBarNode();
+ default_name_ = "http://www.zim-bop-a-dee.com/";
+ default_title_ = ASCIIToUTF16("ooh title");
+ const BookmarkNode* default_node = model->AddURL(default_parent_, 0,
+ default_title_,
+ GURL(default_name_));
+ controller_ = [[BookmarkEditorController alloc]
+ initWithParentWindow:test_window()
+ profile:browser_helper_.profile()
+ parent:default_parent_
+ node:default_node
+ configuration:BookmarkEditor::NO_TREE];
+ [controller_ runAsModalSheet];
+ }
+
+ virtual void TearDown() {
+ controller_ = NULL;
+ CocoaTest::TearDown();
+ }
+};
+
+TEST_F(BookmarkEditorControllerTest, NoEdit) {
+ [controller_ cancel:nil];
+ ASSERT_EQ(default_parent_->GetChildCount(), 1);
+ const BookmarkNode* child = default_parent_->GetChild(0);
+ EXPECT_EQ(child->GetTitle(), default_title_);
+ EXPECT_EQ(child->GetURL(), GURL(default_name_));
+}
+
+TEST_F(BookmarkEditorControllerTest, EditTitle) {
+ [controller_ setDisplayName:@"whamma jamma bamma"];
+ [controller_ ok:nil];
+ ASSERT_EQ(default_parent_->GetChildCount(), 1);
+ const BookmarkNode* child = default_parent_->GetChild(0);
+ EXPECT_EQ(child->GetTitle(), ASCIIToUTF16("whamma jamma bamma"));
+ EXPECT_EQ(child->GetURL(), GURL(default_name_));
+}
+
+TEST_F(BookmarkEditorControllerTest, EditURL) {
+ EXPECT_TRUE([controller_ okButtonEnabled]);
+ [controller_ setDisplayURL:@"http://yellow-sneakers.com/"];
+ EXPECT_TRUE([controller_ okButtonEnabled]);
+ [controller_ ok:nil];
+ ASSERT_EQ(default_parent_->GetChildCount(), 1);
+ const BookmarkNode* child = default_parent_->GetChild(0);
+ EXPECT_EQ(child->GetTitle(), default_title_);
+ EXPECT_EQ(child->GetURL(), GURL("http://yellow-sneakers.com/"));
+}
+
+TEST_F(BookmarkEditorControllerTest, EditAndFixPrefix) {
+ [controller_ setDisplayURL:@"x"];
+ [controller_ ok:nil];
+ ASSERT_EQ(default_parent_->GetChildCount(), 1);
+ const BookmarkNode* child = default_parent_->GetChild(0);
+ EXPECT_TRUE(child->GetURL().is_valid());
+}
+
+TEST_F(BookmarkEditorControllerTest, NodeDeleted) {
+ // Delete the bookmark being edited and verify the sheet cancels itself:
+ ASSERT_TRUE([test_window() attachedSheet]);
+ BookmarkModel* model = browser_helper_.profile()->GetBookmarkModel();
+ model->Remove(default_parent_, 0);
+ ASSERT_FALSE([test_window() attachedSheet]);
+}
+
+TEST_F(BookmarkEditorControllerTest, EditAndConfirmOKButton) {
+ // Confirm OK button enabled/disabled as appropriate:
+ // First test the URL.
+ EXPECT_TRUE([controller_ okButtonEnabled]);
+ [controller_ setDisplayURL:@""];
+ EXPECT_FALSE([controller_ okButtonEnabled]);
+ [controller_ setDisplayURL:@"http://www.cnn.com"];
+ EXPECT_TRUE([controller_ okButtonEnabled]);
+ // Then test the name.
+ [controller_ setDisplayName:@""];
+ EXPECT_TRUE([controller_ okButtonEnabled]);
+ [controller_ setDisplayName:@" "];
+ EXPECT_TRUE([controller_ okButtonEnabled]);
+ // Then little mix of both.
+ [controller_ setDisplayName:@"name"];
+ EXPECT_TRUE([controller_ okButtonEnabled]);
+ [controller_ setDisplayURL:@""];
+ EXPECT_FALSE([controller_ okButtonEnabled]);
+ [controller_ cancel:nil];
+}
+
+TEST_F(BookmarkEditorControllerTest, GoodAndBadURLsChangeColor) {
+ // Confirm that the background color of the URL edit field changes
+ // based on whether it contains a valid or invalid URL.
+ [controller_ setDisplayURL:@"http://www.cnn.com"];
+ NSColor *urlColorA = [controller_ urlFieldColor];
+ EXPECT_TRUE(urlColorA);
+ [controller_ setDisplayURL:@""];
+ NSColor *urlColorB = [controller_ urlFieldColor];
+ EXPECT_TRUE(urlColorB);
+ EXPECT_NSNE(urlColorA, urlColorB);
+ [controller_ setDisplayURL:@"http://www.google.com"];
+ [controller_ cancel:nil];
+ urlColorB = [controller_ urlFieldColor];
+ EXPECT_TRUE(urlColorB);
+ EXPECT_NSEQ(urlColorA, urlColorB);
+}
+
+class BookmarkEditorControllerNoNodeTest : public CocoaTest {
+ public:
+ BrowserTestHelper browser_helper_;
+ BookmarkEditorController* controller_;
+
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ BookmarkModel* model = browser_helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ controller_ = [[BookmarkEditorController alloc]
+ initWithParentWindow:test_window()
+ profile:browser_helper_.profile()
+ parent:parent
+ node:NULL
+ configuration:BookmarkEditor::NO_TREE];
+
+ [controller_ runAsModalSheet];
+ }
+
+ virtual void TearDown() {
+ controller_ = NULL;
+ CocoaTest::TearDown();
+ }
+};
+
+TEST_F(BookmarkEditorControllerNoNodeTest, NoNodeNoTree) {
+ EXPECT_EQ(@"", [controller_ displayName]);
+ EXPECT_EQ(@"", [controller_ displayURL]);
+ EXPECT_FALSE([controller_ okButtonEnabled]);
+ [controller_ cancel:nil];
+}
+
+class BookmarkEditorControllerYesNodeTest : public CocoaTest {
+ public:
+ BrowserTestHelper browser_helper_;
+ string16 default_title_;
+ const char* url_name_;
+ BookmarkEditorController* controller_;
+
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ BookmarkModel* model = browser_helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ default_title_ = ASCIIToUTF16("wooh title");
+ url_name_ = "http://www.zoom-baby-doo-da.com/";
+ const BookmarkNode* node = model->AddURL(parent, 0, default_title_,
+ GURL(url_name_));
+ controller_ = [[BookmarkEditorController alloc]
+ initWithParentWindow:test_window()
+ profile:browser_helper_.profile()
+ parent:parent
+ node:node
+ configuration:BookmarkEditor::NO_TREE];
+
+ [controller_ runAsModalSheet];
+ }
+
+ virtual void TearDown() {
+ controller_ = NULL;
+ CocoaTest::TearDown();
+ }
+};
+
+TEST_F(BookmarkEditorControllerYesNodeTest, YesNodeShowTree) {
+ EXPECT_NSEQ(base::SysUTF16ToNSString(default_title_),
+ [controller_ displayName]);
+ EXPECT_NSEQ([NSString stringWithCString:url_name_
+ encoding:NSUTF8StringEncoding],
+ [controller_ displayURL]);
+ [controller_ cancel:nil];
+}
+
+class BookmarkEditorControllerTreeTest : public CocoaTest {
+
+ public:
+ BrowserTestHelper browser_helper_;
+ BookmarkEditorController* controller_;
+ const BookmarkNode* group_a_;
+ const BookmarkNode* group_b_;
+ const BookmarkNode* group_bb_;
+ const BookmarkNode* group_c_;
+ const BookmarkNode* bookmark_bb_3_;
+ GURL bb3_url_1_;
+ GURL bb3_url_2_;
+
+ BookmarkEditorControllerTreeTest() {
+ // Set up a small bookmark hierarchy, which will look as follows:
+ // a b c d
+ // a-0 b-0 c-0
+ // a-1 bb-0 c-1
+ // a-2 bb-1 c-2
+ // bb-2
+ // bb-3
+ // bb-4
+ // b-1
+ // b-2
+ BookmarkModel& model(*(browser_helper_.profile()->GetBookmarkModel()));
+ const BookmarkNode* root = model.GetBookmarkBarNode();
+ group_a_ = model.AddGroup(root, 0, ASCIIToUTF16("a"));
+ model.AddURL(group_a_, 0, ASCIIToUTF16("a-0"), GURL("http://a-0.com"));
+ model.AddURL(group_a_, 1, ASCIIToUTF16("a-1"), GURL("http://a-1.com"));
+ model.AddURL(group_a_, 2, ASCIIToUTF16("a-2"), GURL("http://a-2.com"));
+
+ group_b_ = model.AddGroup(root, 1, ASCIIToUTF16("b"));
+ model.AddURL(group_b_, 0, ASCIIToUTF16("b-0"), GURL("http://b-0.com"));
+ group_bb_ = model.AddGroup(group_b_, 1, ASCIIToUTF16("bb"));
+ model.AddURL(group_bb_, 0, ASCIIToUTF16("bb-0"), GURL("http://bb-0.com"));
+ model.AddURL(group_bb_, 1, ASCIIToUTF16("bb-1"), GURL("http://bb-1.com"));
+ model.AddURL(group_bb_, 2, ASCIIToUTF16("bb-2"), GURL("http://bb-2.com"));
+
+ // To find it later, this bookmark name must always have a URL
+ // of http://bb-3.com or https://bb-3.com
+ bb3_url_1_ = GURL("http://bb-3.com");
+ bb3_url_2_ = GURL("https://bb-3.com");
+ bookmark_bb_3_ = model.AddURL(group_bb_, 3, ASCIIToUTF16("bb-3"),
+ bb3_url_1_);
+
+ model.AddURL(group_bb_, 4, ASCIIToUTF16("bb-4"), GURL("http://bb-4.com"));
+ model.AddURL(group_b_, 2, ASCIIToUTF16("b-1"), GURL("http://b-2.com"));
+ model.AddURL(group_b_, 3, ASCIIToUTF16("b-2"), GURL("http://b-3.com"));
+
+ group_c_ = model.AddGroup(root, 2, ASCIIToUTF16("c"));
+ model.AddURL(group_c_, 0, ASCIIToUTF16("c-0"), GURL("http://c-0.com"));
+ model.AddURL(group_c_, 1, ASCIIToUTF16("c-1"), GURL("http://c-1.com"));
+ model.AddURL(group_c_, 2, ASCIIToUTF16("c-2"), GURL("http://c-2.com"));
+ model.AddURL(group_c_, 3, ASCIIToUTF16("c-3"), GURL("http://c-3.com"));
+
+ model.AddURL(root, 3, ASCIIToUTF16("d"), GURL("http://d-0.com"));
+ }
+
+ virtual BookmarkEditorController* CreateController() {
+ return [[BookmarkEditorController alloc]
+ initWithParentWindow:test_window()
+ profile:browser_helper_.profile()
+ parent:group_bb_
+ node:bookmark_bb_3_
+ configuration:BookmarkEditor::SHOW_TREE];
+ }
+
+ virtual void SetUp() {
+ controller_ = CreateController();
+ [controller_ runAsModalSheet];
+ }
+
+ virtual void TearDown() {
+ controller_ = NULL;
+ CocoaTest::TearDown();
+ }
+
+ // After changing a node, pointers to the node may be invalid. This
+ // is because the node itself may not be updated; it may removed and
+ // a new one is added in that location. (Implementation detail of
+ // BookmarkEditorController). This method updates the class's
+ // bookmark_bb_3_ so that it points to the new node for testing.
+ void UpdateBB3() {
+ std::vector<const BookmarkNode*> nodes;
+ BookmarkModel* model = browser_helper_.profile()->GetBookmarkModel();
+ model->GetNodesByURL(bb3_url_1_, &nodes);
+ if (nodes.size() == 0)
+ model->GetNodesByURL(bb3_url_2_, &nodes);
+ DCHECK(nodes.size());
+ bookmark_bb_3_ = nodes[0];
+ }
+
+};
+
+TEST_F(BookmarkEditorControllerTreeTest, VerifyBookmarkTestModel) {
+ BookmarkModel& model(*(browser_helper_.profile()->GetBookmarkModel()));
+ model.root_node();
+ const BookmarkNode& root(*model.GetBookmarkBarNode());
+ EXPECT_EQ(4, root.GetChildCount());
+ const BookmarkNode* child = root.GetChild(0);
+ EXPECT_EQ(3, child->GetChildCount());
+ const BookmarkNode* subchild = child->GetChild(0);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ subchild = child->GetChild(1);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ subchild = child->GetChild(2);
+ EXPECT_EQ(0, subchild->GetChildCount());
+
+ child = root.GetChild(1);
+ EXPECT_EQ(4, child->GetChildCount());
+ subchild = child->GetChild(0);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ subchild = child->GetChild(1);
+ EXPECT_EQ(5, subchild->GetChildCount());
+ const BookmarkNode* subsubchild = subchild->GetChild(0);
+ EXPECT_EQ(0, subsubchild->GetChildCount());
+ subsubchild = subchild->GetChild(1);
+ EXPECT_EQ(0, subsubchild->GetChildCount());
+ subsubchild = subchild->GetChild(2);
+ EXPECT_EQ(0, subsubchild->GetChildCount());
+ subsubchild = subchild->GetChild(3);
+ EXPECT_EQ(0, subsubchild->GetChildCount());
+ subsubchild = subchild->GetChild(4);
+ EXPECT_EQ(0, subsubchild->GetChildCount());
+ subchild = child->GetChild(2);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ subchild = child->GetChild(3);
+ EXPECT_EQ(0, subchild->GetChildCount());
+
+ child = root.GetChild(2);
+ EXPECT_EQ(4, child->GetChildCount());
+ subchild = child->GetChild(0);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ subchild = child->GetChild(1);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ subchild = child->GetChild(2);
+ EXPECT_EQ(0, subchild->GetChildCount());
+ subchild = child->GetChild(3);
+ EXPECT_EQ(0, subchild->GetChildCount());
+
+ child = root.GetChild(3);
+ EXPECT_EQ(0, child->GetChildCount());
+ [controller_ cancel:nil];
+}
+
+TEST_F(BookmarkEditorControllerTreeTest, RenameBookmarkInPlace) {
+ const BookmarkNode* oldParent = bookmark_bb_3_->GetParent();
+ [controller_ setDisplayName:@"NEW NAME"];
+ [controller_ ok:nil];
+ UpdateBB3();
+ const BookmarkNode* newParent = bookmark_bb_3_->GetParent();
+ ASSERT_EQ(newParent, oldParent);
+ int childIndex = newParent->IndexOfChild(bookmark_bb_3_);
+ ASSERT_EQ(3, childIndex);
+}
+
+TEST_F(BookmarkEditorControllerTreeTest, ChangeBookmarkURLInPlace) {
+ const BookmarkNode* oldParent = bookmark_bb_3_->GetParent();
+ [controller_ setDisplayURL:@"https://bb-3.com"];
+ [controller_ ok:nil];
+ UpdateBB3();
+ const BookmarkNode* newParent = bookmark_bb_3_->GetParent();
+ ASSERT_EQ(newParent, oldParent);
+ int childIndex = newParent->IndexOfChild(bookmark_bb_3_);
+ ASSERT_EQ(3, childIndex);
+}
+
+TEST_F(BookmarkEditorControllerTreeTest, ChangeBookmarkGroup) {
+ [controller_ selectTestNodeInBrowser:group_c_];
+ [controller_ ok:nil];
+ UpdateBB3();
+ const BookmarkNode* parent = bookmark_bb_3_->GetParent();
+ ASSERT_EQ(parent, group_c_);
+ int childIndex = parent->IndexOfChild(bookmark_bb_3_);
+ ASSERT_EQ(4, childIndex);
+}
+
+TEST_F(BookmarkEditorControllerTreeTest, ChangeNameAndBookmarkGroup) {
+ [controller_ setDisplayName:@"NEW NAME"];
+ [controller_ selectTestNodeInBrowser:group_c_];
+ [controller_ ok:nil];
+ UpdateBB3();
+ const BookmarkNode* parent = bookmark_bb_3_->GetParent();
+ ASSERT_EQ(parent, group_c_);
+ int childIndex = parent->IndexOfChild(bookmark_bb_3_);
+ ASSERT_EQ(4, childIndex);
+ EXPECT_EQ(bookmark_bb_3_->GetTitle(), ASCIIToUTF16("NEW NAME"));
+}
+
+TEST_F(BookmarkEditorControllerTreeTest, AddFolderWithGroupSelected) {
+ // Folders are NOT added unless the OK button is pressed.
+ [controller_ newFolder:nil];
+ [controller_ cancel:nil];
+ EXPECT_EQ(5, group_bb_->GetChildCount());
+}
+
+class BookmarkEditorControllerTreeNoNodeTest :
+ public BookmarkEditorControllerTreeTest {
+ public:
+ virtual BookmarkEditorController* CreateController() {
+ return [[BookmarkEditorController alloc]
+ initWithParentWindow:test_window()
+ profile:browser_helper_.profile()
+ parent:group_bb_
+ node:nil
+ configuration:BookmarkEditor::SHOW_TREE];
+ }
+
+};
+
+TEST_F(BookmarkEditorControllerTreeNoNodeTest, NewBookmarkNoNode) {
+ [controller_ setDisplayName:@"NEW BOOKMARK"];
+ [controller_ setDisplayURL:@"http://NEWURL.com"];
+ [controller_ ok:nil];
+ const BookmarkNode* new_node = group_bb_->GetChild(5);
+ ASSERT_EQ(0, new_node->GetChildCount());
+ EXPECT_EQ(new_node->GetTitle(), ASCIIToUTF16("NEW BOOKMARK"));
+ EXPECT_EQ(new_node->GetURL(), GURL("http://NEWURL.com"));
+}
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h
new file mode 100644
index 0000000..e2af266
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h
@@ -0,0 +1,50 @@
+// 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_UI_COCOA_BOOKMARKS_BOOKMARK_FOLDER_TARGET_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_FOLDER_TARGET_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+@class BookmarkButton;
+@protocol BookmarkButtonControllerProtocol;
+class BookmarkNode;
+
+// Target (in the target/action sense) of a bookmark folder button.
+// Since ObjC doesn't have multiple inheritance we use has-a instead
+// of is-a to share behavior between the BookmarkBarFolderController
+// (NSWindowController) and the BookmarkBarController
+// (NSViewController).
+//
+// This class is unit tested in the context of a BookmarkBarController.
+@interface BookmarkFolderTarget : NSObject {
+ // The owner of the bookmark folder button
+ id<BookmarkButtonControllerProtocol> controller_; // weak
+}
+
+- (id)initWithController:(id<BookmarkButtonControllerProtocol>)controller;
+
+// Main IBAction for a button click.
+- (IBAction)openBookmarkFolderFromButton:(id)sender;
+
+// Copies the given bookmark node to the given pasteboard, declaring appropriate
+// types (to paste a URL with a title).
+- (void)copyBookmarkNode:(const BookmarkNode*)node
+ toPasteboard:(NSPasteboard*)pboard;
+
+// 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;
+
+@end
+
+// The (internal) |NSPasteboard| type string for bookmark button drags, used for
+// dragging buttons around the bookmark bar. The data for this type is just a
+// pointer to the |BookmarkButton| being dragged.
+extern NSString* kBookmarkButtonDragType;
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_FOLDER_TARGET_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.mm
new file mode 100644
index 0000000..95531b2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.mm
@@ -0,0 +1,118 @@
+// 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/ui/cocoa/bookmarks/bookmark_folder_target.h"
+
+#include "base/logging.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
+#import "chrome/browser/ui/cocoa/event_utils.h"
+#import "third_party/mozilla/NSPasteboard+Utils.h"
+
+NSString* kBookmarkButtonDragType = @"ChromiumBookmarkButtonDragType";
+
+@implementation BookmarkFolderTarget
+
+- (id)initWithController:(id<BookmarkButtonControllerProtocol>)controller {
+ if ((self = [super init])) {
+ controller_ = controller;
+ }
+ return self;
+}
+
+// This IBAction is called when the user clicks (mouseUp, really) on a
+// "folder" bookmark button. (In this context, "Click" does not
+// include right-click to open a context menu which follows a
+// different path). Scenarios when folder X is clicked:
+// *Predicate* *Action*
+// (nothing) Open Folder X
+// Folder X open Close folder X
+// Folder Y open Close Y, open X
+// Cmd-click Open All with proper disposition
+//
+// Note complication in which a click-drag engages drag and drop, not
+// a click-to-open. Thus the path to get here is a little twisted.
+- (IBAction)openBookmarkFolderFromButton:(id)sender {
+ DCHECK(sender);
+ // Watch out for a modifier click. For example, command-click
+ // should open all.
+ //
+ // NOTE: we cannot use [[sender cell] mouseDownFlags] because we
+ // thwart the normal mouse click mechanism to make buttons
+ // draggable. Thus we must use [NSApp currentEvent].
+ //
+ // Holding command while using the scroll wheel (or moving around
+ // over a bookmark folder) can confuse us. Unless we check the
+ // event type, we are not sure if this is an "open folder" due to a
+ // hover-open or "open folder" due to a click. It doesn't matter
+ // (both do the same thing) unless a modifier is held, since
+ // command-click should "open all" but command-move should not.
+ // WindowOpenDispositionFromNSEvent does not consider the event
+ // type; only the modifiers. Thus the need for an extra
+ // event-type-check here.
+ DCHECK([sender bookmarkNode]->is_folder());
+ NSEvent* event = [NSApp currentEvent];
+ WindowOpenDisposition disposition =
+ event_utils::WindowOpenDispositionFromNSEvent(event);
+ if (([event type] != NSMouseEntered) &&
+ ([event type] != NSMouseMoved) &&
+ ([event type] != NSScrollWheel) &&
+ (disposition == NEW_BACKGROUND_TAB)) {
+ [controller_ closeAllBookmarkFolders];
+ [controller_ openAll:[sender bookmarkNode] disposition:disposition];
+ return;
+ }
+
+ // If click on same folder, close it and be done.
+ // Else we clicked on a different folder so more work to do.
+ if ([[controller_ folderController] parentButton] == sender) {
+ [controller_ closeBookmarkFolder:controller_];
+ return;
+ }
+
+ [controller_ addNewFolderControllerWithParentButton:sender];
+}
+
+- (void)copyBookmarkNode:(const BookmarkNode*)node
+ toPasteboard:(NSPasteboard*)pboard {
+ if (!node) {
+ NOTREACHED();
+ return;
+ }
+
+ if (node->is_folder()) {
+ // TODO(viettrungluu): I'm not sure what we should do, so just declare the
+ // "additional" types we're given for now. Maybe we want to add a list of
+ // URLs? Would we then have to recurse if there were subfolders?
+ // In the meanwhile, we *must* set it to a known state. (If this survives to
+ // a 10.6-only release, it can be replaced with |-clearContents|.)
+ [pboard declareTypes:[NSArray array] owner:nil];
+ } else {
+ const std::string spec = node->GetURL().spec();
+ NSString* url = base::SysUTF8ToNSString(spec);
+ NSString* title = base::SysUTF16ToNSString(node->GetTitle());
+ [pboard declareURLPasteboardWithAdditionalTypes:[NSArray array]
+ owner:nil];
+ [pboard setDataForURL:url title:title];
+ }
+}
+
+- (void)fillPasteboard:(NSPasteboard*)pboard
+ forDragOfButton:(BookmarkButton*)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 toPasteboard:pboard];
+ [pboard addTypes:[NSArray arrayWithObject:kBookmarkButtonDragType]
+ owner:nil];
+ [pboard setData:[NSData dataWithBytes:&button length:sizeof(button)]
+ forType:kBookmarkButtonDragType];
+ } else {
+ NOTREACHED();
+ }
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target_unittest.mm
new file mode 100644
index 0000000..0142bfb
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target_unittest.mm
@@ -0,0 +1,125 @@
+// 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 "chrome/browser/bookmarks/bookmark_model.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h"
+#include "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+
+@interface OCMockObject(PreventRetainCycle)
+- (void)clearRecordersAndExpectations;
+@end
+
+@implementation OCMockObject(PreventRetainCycle)
+
+// We need a mechanism to clear the invocation handlers to break a
+// retain cycle (see below; search for "retain cycle").
+- (void)clearRecordersAndExpectations {
+ [recorders removeAllObjects];
+ [expectations removeAllObjects];
+}
+
+@end
+
+
+class BookmarkFolderTargetTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ bmbNode_ = model->GetBookmarkBarNode();
+ }
+ virtual void TearDown() {
+ pool_.Recycle();
+ CocoaTest::TearDown();
+ }
+
+ BrowserTestHelper helper_;
+ const BookmarkNode* bmbNode_;
+ base::mac::ScopedNSAutoreleasePool pool_;
+};
+
+TEST_F(BookmarkFolderTargetTest, StartWithNothing) {
+ // Need a fake "button" which has a bookmark node.
+ id sender = [OCMockObject mockForClass:[BookmarkButton class]];
+ [[[sender stub] andReturnValue:OCMOCK_VALUE(bmbNode_)] bookmarkNode];
+
+ // Fake controller
+ id controller = [OCMockObject mockForClass:[BookmarkBarFolderController
+ class]];
+ // No current folder
+ [[[controller stub] andReturn:nil] folderController];
+
+ // Make sure we get an addNew
+ [[controller expect] addNewFolderControllerWithParentButton:sender];
+
+ scoped_nsobject<BookmarkFolderTarget> target(
+ [[BookmarkFolderTarget alloc] initWithController:controller]);
+
+ [target openBookmarkFolderFromButton:sender];
+ [controller verify];
+}
+
+TEST_F(BookmarkFolderTargetTest, ReopenSameFolder) {
+ // Need a fake "button" which has a bookmark node.
+ id sender = [OCMockObject mockForClass:[BookmarkButton class]];
+ [[[sender stub] andReturnValue:OCMOCK_VALUE(bmbNode_)] bookmarkNode];
+
+ // Fake controller
+ id controller = [OCMockObject mockForClass:[BookmarkBarFolderController
+ class]];
+ // YES a current folder. Self-mock that as well, so "same" will be
+ // true. Note this creates a retain cycle in OCMockObject; we
+ // accomodate at the end of this function.
+ [[[controller stub] andReturn:controller] folderController];
+ [[[controller stub] andReturn:sender] parentButton];
+
+ // The folder is open, so a click should close just that folder (and
+ // any subfolders).
+ [[controller expect] closeBookmarkFolder:controller];
+
+ scoped_nsobject<BookmarkFolderTarget> target(
+ [[BookmarkFolderTarget alloc] initWithController:controller]);
+
+ [target openBookmarkFolderFromButton:sender];
+ [controller verify];
+
+ // Our use of OCMockObject means an object can return itself. This
+ // creates a retain cycle, since OCMock retains all objects used in
+ // mock creation. Clear out the invocation handlers of all
+ // OCMockRecorders we used to break the cycles.
+ [controller clearRecordersAndExpectations];
+}
+
+TEST_F(BookmarkFolderTargetTest, ReopenNotSame) {
+ // Need a fake "button" which has a bookmark node.
+ id sender = [OCMockObject mockForClass:[BookmarkButton class]];
+ [[[sender stub] andReturnValue:OCMOCK_VALUE(bmbNode_)] bookmarkNode];
+
+ // Fake controller
+ id controller = [OCMockObject mockForClass:[BookmarkBarFolderController
+ class]];
+ // YES a current folder but NOT same.
+ [[[controller stub] andReturn:controller] folderController];
+ [[[controller stub] andReturn:nil] parentButton];
+
+ // Insure the controller gets a chance to decide which folders to
+ // close and open.
+ [[controller expect] addNewFolderControllerWithParentButton:sender];
+
+ scoped_nsobject<BookmarkFolderTarget> target(
+ [[BookmarkFolderTarget alloc] initWithController:controller]);
+
+ [target openBookmarkFolderFromButton:sender];
+ [controller verify];
+
+ // Break retain cycles.
+ [controller clearRecordersAndExpectations];
+}
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h
new file mode 100644
index 0000000..d4ac001
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h
@@ -0,0 +1,20 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/basictypes.h"
+
+
+// The context menu for bookmark buttons needs to know which
+// BookmarkNode it is talking about. For example, "Open All" is
+// disabled if the bookmark node is a folder and has no children.
+@interface BookmarkMenu : NSMenu {
+ @private
+ int64 id_; // id of the bookmark node we represent.
+}
+- (void)setRepresentedObject:(id)object;
+@property (nonatomic) int64 id;
+@end
+
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu.mm
new file mode 100644
index 0000000..274edc7
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu.mm
@@ -0,0 +1,22 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h"
+
+
+@implementation BookmarkMenu
+
+@synthesize id = id_;
+
+// Convention in the bookmark bar controller: the bookmark button
+// cells have a BookmarkNode as their represented object. This object
+// is placed in a BookmarkMenu at the time a cell is asked for its
+// menu.
+- (void)setRepresentedObject:(id)object {
+ if ([object isKindOfClass:[NSNumber class]]) {
+ id_ = static_cast<int64>([object longLongValue]);
+ }
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h
new file mode 100644
index 0000000..db64b2c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h
@@ -0,0 +1,123 @@
+// 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.
+
+// C++ controller for the bookmark menu; one per AppController (which
+// means there is only one). When bookmarks are changed, this class
+// takes care of updating Cocoa bookmark menus. This is not named
+// BookmarkMenuController to help avoid confusion between languages.
+// This class needs to be C++, not ObjC, since it derives from
+// BookmarkModelObserver.
+//
+// Most Chromium Cocoa menu items are static from a nib (e.g. New
+// Tab), but may be enabled/disabled under certain circumstances
+// (e.g. Cut and Paste). In addition, most Cocoa menu items have
+// firstResponder: as a target. Unusually, bookmark menu items are
+// created dynamically. They also have a target of
+// BookmarkMenuCocoaController instead of firstResponder.
+// See BookmarkMenuBridge::AddNodeToMenu()).
+
+#ifndef CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MENU_BRIDGE_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MENU_BRIDGE_H_
+#pragma once
+
+#include <map>
+
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/bookmarks/bookmark_model_observer.h"
+
+class BookmarkNode;
+class Profile;
+@class NSImage;
+@class NSMenu;
+@class NSMenuItem;
+@class BookmarkMenuCocoaController;
+
+class BookmarkMenuBridge : public BookmarkModelObserver {
+ public:
+ BookmarkMenuBridge(Profile* profile);
+ virtual ~BookmarkMenuBridge();
+
+ // Overridden from BookmarkModelObserver
+ virtual void Loaded(BookmarkModel* model);
+ virtual void BookmarkModelBeingDeleted(BookmarkModel* model);
+ virtual void BookmarkNodeMoved(BookmarkModel* model,
+ const BookmarkNode* old_parent,
+ int old_index,
+ const BookmarkNode* new_parent,
+ int new_index);
+ virtual void BookmarkNodeAdded(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int index);
+ virtual void BookmarkNodeRemoved(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int old_index,
+ const BookmarkNode* node);
+ virtual void BookmarkNodeChanged(BookmarkModel* model,
+ const BookmarkNode* node);
+ virtual void BookmarkNodeFavIconLoaded(BookmarkModel* model,
+ const BookmarkNode* node);
+ virtual void BookmarkNodeChildrenReordered(BookmarkModel* model,
+ const BookmarkNode* node);
+
+ // Rebuilds the bookmark menu, if it has been marked invalid.
+ void UpdateMenu(NSMenu* bookmark_menu);
+
+ // I wish I had a "friend @class" construct.
+ BookmarkModel* GetBookmarkModel();
+ Profile* GetProfile();
+
+ protected:
+ // Clear all bookmarks from the given bookmark menu.
+ void ClearBookmarkMenu(NSMenu* menu);
+
+ // Mark the bookmark menu as being invalid.
+ void InvalidateMenu() { menuIsValid_ = false; }
+
+ // Helper for adding the node as a submenu to the menu with the
+ // given title.
+ void AddNodeAsSubmenu(NSMenu* menu,
+ const BookmarkNode* node,
+ NSString* title);
+
+ // Helper for recursively adding items to our bookmark menu
+ // All children of |node| will be added to |menu|.
+ // TODO(jrg): add a counter to enforce maximum nodes added
+ void AddNodeToMenu(const BookmarkNode* node, NSMenu* menu);
+
+ // This configures an NSMenuItem with all the data from a BookmarkNode. This
+ // is used to update existing menu items, as well as to configure newly
+ // created ones, like in AddNodeToMenu().
+ // |set_title| is optional since it is only needed when we get a
+ // node changed notification. On initial build of the menu we set
+ // the title as part of alloc/init.
+ void ConfigureMenuItem(const BookmarkNode* node, NSMenuItem* item,
+ bool set_title);
+
+ // Returns the NSMenuItem for a given BookmarkNode.
+ NSMenuItem* MenuItemForNode(const BookmarkNode* node);
+
+ // Return the Bookmark menu.
+ virtual NSMenu* BookmarkMenu();
+
+ // Start watching the bookmarks for changes.
+ void ObserveBookmarkModel();
+
+ private:
+ friend class BookmarkMenuBridgeTest;
+
+ // True iff the menu is up-to-date with the actual BookmarkModel.
+ bool menuIsValid_;
+
+ Profile* profile_; // weak
+ BookmarkMenuCocoaController* controller_; // strong
+
+ // The folder image so we can use one copy for all.
+ scoped_nsobject<NSImage> folder_image_;
+
+ // In order to appropriately update items in the bookmark menu, without
+ // forcing a rebuild, map the model's nodes to menu items.
+ std::map<const BookmarkNode*, NSMenuItem*> bookmark_nodes_;
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MENU_BRIDGE_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.mm
new file mode 100644
index 0000000..fbe5f10
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.mm
@@ -0,0 +1,253 @@
+// 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 <AppKit/AppKit.h>
+
+#include "app/l10n_util.h"
+#include "app/resource_bundle.h"
+#include "base/nsimage_cache_mac.h"
+#include "base/sys_string_conversions.h"
+#import "chrome/browser/app_controller_mac.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/profile_manager.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+
+BookmarkMenuBridge::BookmarkMenuBridge(Profile* profile)
+ : menuIsValid_(false),
+ profile_(profile),
+ controller_([[BookmarkMenuCocoaController alloc] initWithBridge:this]) {
+ if (GetBookmarkModel())
+ ObserveBookmarkModel();
+}
+
+BookmarkMenuBridge::~BookmarkMenuBridge() {
+ BookmarkModel *model = GetBookmarkModel();
+ if (model)
+ model->RemoveObserver(this);
+ [controller_ release];
+}
+
+NSMenu* BookmarkMenuBridge::BookmarkMenu() {
+ return [controller_ menu];
+}
+
+void BookmarkMenuBridge::Loaded(BookmarkModel* model) {
+ InvalidateMenu();
+}
+
+void BookmarkMenuBridge::UpdateMenu(NSMenu* bookmark_menu) {
+ DCHECK(bookmark_menu);
+ if (menuIsValid_)
+ return;
+ BookmarkModel* model = GetBookmarkModel();
+ if (!model || !model->IsLoaded())
+ return;
+
+ if (!folder_image_) {
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ folder_image_.reset(
+ [rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER) retain]);
+ }
+
+ ClearBookmarkMenu(bookmark_menu);
+
+ // Add bookmark bar items, if any.
+ const BookmarkNode* barNode = model->GetBookmarkBarNode();
+ CHECK(barNode);
+ if (barNode->GetChildCount()) {
+ [bookmark_menu addItem:[NSMenuItem separatorItem]];
+ AddNodeToMenu(barNode, bookmark_menu);
+ }
+
+ // Create a submenu for "other bookmarks", and fill it in.
+ NSString* other_items_title =
+ l10n_util::GetNSString(IDS_BOOMARK_BAR_OTHER_FOLDER_NAME);
+ [bookmark_menu addItem:[NSMenuItem separatorItem]];
+ AddNodeAsSubmenu(bookmark_menu,
+ model->other_node(),
+ other_items_title);
+
+ menuIsValid_ = true;
+}
+
+void BookmarkMenuBridge::BookmarkModelBeingDeleted(BookmarkModel* model) {
+ NSMenu* bookmark_menu = BookmarkMenu();
+ if (bookmark_menu == nil)
+ return;
+
+ ClearBookmarkMenu(bookmark_menu);
+}
+
+void BookmarkMenuBridge::BookmarkNodeMoved(BookmarkModel* model,
+ const BookmarkNode* old_parent,
+ int old_index,
+ const BookmarkNode* new_parent,
+ int new_index) {
+ InvalidateMenu();
+}
+
+void BookmarkMenuBridge::BookmarkNodeAdded(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int index) {
+ InvalidateMenu();
+}
+
+void BookmarkMenuBridge::BookmarkNodeRemoved(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int old_index,
+ const BookmarkNode* node) {
+ InvalidateMenu();
+}
+
+void BookmarkMenuBridge::BookmarkNodeChanged(BookmarkModel* model,
+ const BookmarkNode* node) {
+ NSMenuItem* item = MenuItemForNode(node);
+ if (item)
+ ConfigureMenuItem(node, item, true);
+}
+
+void BookmarkMenuBridge::BookmarkNodeFavIconLoaded(BookmarkModel* model,
+ const BookmarkNode* node) {
+ NSMenuItem* item = MenuItemForNode(node);
+ if (item)
+ ConfigureMenuItem(node, item, false);
+}
+
+void BookmarkMenuBridge::BookmarkNodeChildrenReordered(
+ BookmarkModel* model, const BookmarkNode* node) {
+ InvalidateMenu();
+}
+
+// Watch for changes.
+void BookmarkMenuBridge::ObserveBookmarkModel() {
+ BookmarkModel* model = GetBookmarkModel();
+ model->AddObserver(this);
+ if (model->IsLoaded())
+ Loaded(model);
+}
+
+BookmarkModel* BookmarkMenuBridge::GetBookmarkModel() {
+ if (!profile_)
+ return NULL;
+ return profile_->GetBookmarkModel();
+}
+
+Profile* BookmarkMenuBridge::GetProfile() {
+ return profile_;
+}
+
+void BookmarkMenuBridge::ClearBookmarkMenu(NSMenu* menu) {
+ bookmark_nodes_.clear();
+ // Recursively delete all menus that look like a bookmark. Assume
+ // all items with submenus contain only bookmarks. Also delete all
+ // separator items since we explicirly add them back in. This should
+ // deletes everything except the first item ("Add Bookmark...").
+ NSArray* items = [menu itemArray];
+ for (NSMenuItem* item in items) {
+ // Convention: items in the bookmark list which are bookmarks have
+ // an action of openBookmarkMenuItem:. Also, assume all items
+ // with submenus are submenus of bookmarks.
+ if (([item action] == @selector(openBookmarkMenuItem:)) ||
+ [item hasSubmenu] ||
+ [item isSeparatorItem]) {
+ // This will eventually [obj release] all its kids, if it has
+ // any.
+ [menu removeItem:item];
+ } else {
+ // Leave it alone.
+ }
+ }
+}
+
+void BookmarkMenuBridge::AddNodeAsSubmenu(NSMenu* menu,
+ const BookmarkNode* node,
+ NSString* title) {
+ NSMenuItem* items = [[[NSMenuItem alloc]
+ initWithTitle:title
+ action:nil
+ keyEquivalent:@""] autorelease];
+ [items setImage:folder_image_];
+ [menu addItem:items];
+ NSMenu* other_submenu = [[[NSMenu alloc] initWithTitle:title]
+ autorelease];
+ [menu setSubmenu:other_submenu forItem:items];
+ AddNodeToMenu(node, other_submenu);
+}
+
+// TODO(jrg): limit the number of bookmarks in the menubar?
+void BookmarkMenuBridge::AddNodeToMenu(const BookmarkNode* node, NSMenu* menu) {
+ int child_count = node->GetChildCount();
+ if (!child_count) {
+ NSString* empty_string = l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU);
+ NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:empty_string
+ action:nil
+ keyEquivalent:@""] autorelease];
+ [menu addItem:item];
+ } else for (int i = 0; i < child_count; i++) {
+ const BookmarkNode* child = node->GetChild(i);
+ NSString* title = [BookmarkMenuCocoaController menuTitleForNode:child];
+ NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:title
+ action:nil
+ keyEquivalent:@""] autorelease];
+ [menu addItem:item];
+ bookmark_nodes_[child] = item;
+ if (child->is_folder()) {
+ [item setImage:folder_image_];
+ NSMenu* submenu = [[[NSMenu alloc] initWithTitle:title] autorelease];
+ [menu setSubmenu:submenu forItem:item];
+ AddNodeToMenu(child, submenu); // recursive call
+ } else {
+ ConfigureMenuItem(child, item, false);
+ }
+ }
+}
+
+void BookmarkMenuBridge::ConfigureMenuItem(const BookmarkNode* node,
+ NSMenuItem* item,
+ bool set_title) {
+ if (set_title) {
+ NSString* title = [BookmarkMenuCocoaController menuTitleForNode:node];
+ [item setTitle:title];
+ }
+ [item setTarget:controller_];
+ [item setAction:@selector(openBookmarkMenuItem:)];
+ [item setTag:node->id()];
+ // Add a tooltip
+ std::string url_string = node->GetURL().possibly_invalid_spec();
+ NSString* tooltip = [NSString stringWithFormat:@"%@\n%s",
+ base::SysUTF16ToNSString(node->GetTitle()),
+ url_string.c_str()];
+ [item setToolTip:tooltip];
+
+ // Check to see if we have a favicon.
+ NSImage* favicon = nil;
+ BookmarkModel* model = GetBookmarkModel();
+ if (model) {
+ const SkBitmap& bitmap = model->GetFavIcon(node);
+ if (!bitmap.isNull())
+ favicon = gfx::SkBitmapToNSImage(bitmap);
+ }
+ // Either we do not have a loaded favicon or the conversion from SkBitmap
+ // failed. Use the default site image instead.
+ if (!favicon)
+ favicon = nsimage_cache::ImageNamed(@"nav.pdf");
+ [item setImage:favicon];
+}
+
+NSMenuItem* BookmarkMenuBridge::MenuItemForNode(const BookmarkNode* node) {
+ if (!node)
+ return nil;
+ std::map<const BookmarkNode*, NSMenuItem*>::iterator it =
+ bookmark_nodes_.find(node);
+ if (it == bookmark_nodes_.end())
+ return nil;
+ return it->second;
+}
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge_unittest.mm
new file mode 100644
index 0000000..cec14eb
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge_unittest.mm
@@ -0,0 +1,317 @@
+// 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 <AppKit/AppKit.h>
+
+#import "base/scoped_nsobject.h"
+#include "base/string16.h"
+#include "base/string_util.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/browser.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+class TestBookmarkMenuBridge : public BookmarkMenuBridge {
+ public:
+ TestBookmarkMenuBridge(Profile* profile)
+ : BookmarkMenuBridge(profile),
+ menu_([[NSMenu alloc] initWithTitle:@"test"]) {
+ }
+ virtual ~TestBookmarkMenuBridge() {}
+
+ scoped_nsobject<NSMenu> menu_;
+
+ protected:
+ // Overridden from BookmarkMenuBridge.
+ virtual NSMenu* BookmarkMenu() {
+ return menu_;
+ }
+};
+
+// TODO(jrg): see refactor comment in bookmark_bar_state_controller_unittest.mm
+class BookmarkMenuBridgeTest : public PlatformTest {
+ public:
+
+ void SetUp() {
+ bridge_.reset(new TestBookmarkMenuBridge(browser_test_helper_.profile()));
+ EXPECT_TRUE(bridge_.get());
+ }
+
+ // We are a friend of BookmarkMenuBridge (and have access to
+ // protected methods), but none of the classes generated by TEST_F()
+ // are. This (and AddNodeToMenu()) are simple wrappers to let
+ // derived test classes have access to protected methods.
+ void ClearBookmarkMenu(BookmarkMenuBridge* bridge, NSMenu* menu) {
+ bridge->ClearBookmarkMenu(menu);
+ }
+
+ void InvalidateMenu() { bridge_->InvalidateMenu(); }
+ bool menu_is_valid() { return bridge_->menuIsValid_; }
+
+ void AddNodeToMenu(BookmarkMenuBridge* bridge, const BookmarkNode* root,
+ NSMenu* menu) {
+ bridge->AddNodeToMenu(root, menu);
+ }
+
+ NSMenuItem* MenuItemForNode(BookmarkMenuBridge* bridge,
+ const BookmarkNode* node) {
+ return bridge->MenuItemForNode(node);
+ }
+
+ NSMenuItem* AddItemToMenu(NSMenu *menu, NSString *title, SEL selector) {
+ NSMenuItem *item = [[[NSMenuItem alloc] initWithTitle:title action:NULL
+ keyEquivalent:@""] autorelease];
+ if (selector)
+ [item setAction:selector];
+ [menu addItem:item];
+ return item;
+ }
+
+ BrowserTestHelper browser_test_helper_;
+ scoped_ptr<TestBookmarkMenuBridge> bridge_;
+};
+
+TEST_F(BookmarkMenuBridgeTest, TestBookmarkMenuAutoSeparator) {
+ BookmarkModel* model = bridge_->GetBookmarkModel();
+ bridge_->Loaded(model);
+ NSMenu* menu = bridge_->menu_.get();
+ bridge_->UpdateMenu(menu);
+ // The bare menu after loading has a separator and an "Other Bookmarks"
+ // submenu.
+ EXPECT_EQ(2, [menu numberOfItems]);
+ // Add a bookmark and reload and there should be 4 items: the previous
+ // menu contents plus a new separator and the new bookmark.
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ const char* url = "http://www.zim-bop-a-dee.com/";
+ model->AddURL(parent, 0, ASCIIToUTF16("Bookmark"), GURL(url));
+ bridge_->UpdateMenu(menu);
+ EXPECT_EQ(4, [menu numberOfItems]);
+ // Remove the new bookmark and reload and we should have 2 items again
+ // because the separator should have been removed as well.
+ model->Remove(parent, 0);
+ bridge_->UpdateMenu(menu);
+ EXPECT_EQ(2, [menu numberOfItems]);
+}
+
+// Test that ClearBookmarkMenu() removes all bookmark menus.
+TEST_F(BookmarkMenuBridgeTest, TestClearBookmarkMenu) {
+ NSMenu* menu = bridge_->menu_.get();
+
+ AddItemToMenu(menu, @"hi mom", nil);
+ AddItemToMenu(menu, @"not", @selector(openBookmarkMenuItem:));
+ NSMenuItem* item = AddItemToMenu(menu, @"hi mom", nil);
+ [item setSubmenu:[[[NSMenu alloc] initWithTitle:@"bar"] autorelease]];
+ AddItemToMenu(menu, @"not", @selector(openBookmarkMenuItem:));
+ AddItemToMenu(menu, @"zippy", @selector(length));
+ [menu addItem:[NSMenuItem separatorItem]];
+
+ ClearBookmarkMenu(bridge_.get(), menu);
+
+ // Make sure all bookmark items are removed, all items with
+ // submenus removed, and all separator items are gone.
+ EXPECT_EQ(2, [menu numberOfItems]);
+ for (NSMenuItem *item in [menu itemArray]) {
+ EXPECT_NSNE(@"not", [item title]);
+ }
+}
+
+// Test invalidation
+TEST_F(BookmarkMenuBridgeTest, TestInvalidation) {
+ BookmarkModel* model = bridge_->GetBookmarkModel();
+ bridge_->Loaded(model);
+
+ EXPECT_FALSE(menu_is_valid());
+ bridge_->UpdateMenu(bridge_->menu_);
+ EXPECT_TRUE(menu_is_valid());
+
+ InvalidateMenu();
+ EXPECT_FALSE(menu_is_valid());
+ InvalidateMenu();
+ EXPECT_FALSE(menu_is_valid());
+ bridge_->UpdateMenu(bridge_->menu_);
+ EXPECT_TRUE(menu_is_valid());
+ bridge_->UpdateMenu(bridge_->menu_);
+ EXPECT_TRUE(menu_is_valid());
+
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ const char* url = "http://www.zim-bop-a-dee.com/";
+ model->AddURL(parent, 0, ASCIIToUTF16("Bookmark"), GURL(url));
+
+ EXPECT_FALSE(menu_is_valid());
+ bridge_->UpdateMenu(bridge_->menu_);
+ EXPECT_TRUE(menu_is_valid());
+}
+
+// Test that AddNodeToMenu() properly adds bookmark nodes as menus,
+// including the recursive case.
+TEST_F(BookmarkMenuBridgeTest, TestAddNodeToMenu) {
+ string16 empty;
+ NSMenu* menu = bridge_->menu_.get();
+
+ BookmarkModel* model = bridge_->GetBookmarkModel();
+ const BookmarkNode* root = model->GetBookmarkBarNode();
+ EXPECT_TRUE(model && root);
+
+ const char* short_url = "http://foo/";
+ const char* long_url = "http://super-duper-long-url--."
+ "that.cannot.possibly.fit.even-in-80-columns"
+ "or.be.reasonably-displayed-in-a-menu"
+ "without.looking-ridiculous.com/"; // 140 chars total
+
+ // 3 nodes; middle one has a child, last one has a HUGE URL
+ // Set their titles to be the same as the URLs
+ const BookmarkNode* node = NULL;
+ model->AddURL(root, 0, ASCIIToUTF16(short_url), GURL(short_url));
+ bridge_->UpdateMenu(menu);
+ int prev_count = [menu numberOfItems] - 1; // "extras" added at this point
+ node = model->AddGroup(root, 1, empty);
+ model->AddURL(root, 2, ASCIIToUTF16(long_url), GURL(long_url));
+
+ // And the submenu fo the middle one
+ model->AddURL(node, 0, empty, GURL("http://sub"));
+ bridge_->UpdateMenu(menu);
+
+ EXPECT_EQ((NSInteger)(prev_count+3), [menu numberOfItems]);
+
+ // Verify the 1st one is there with the right action.
+ NSMenuItem* item = [menu itemWithTitle:[NSString
+ stringWithUTF8String:short_url]];
+ EXPECT_TRUE(item);
+ EXPECT_EQ(@selector(openBookmarkMenuItem:), [item action]);
+ EXPECT_EQ(NO, [item hasSubmenu]);
+ NSMenuItem* short_item = item;
+ NSMenuItem* long_item = nil;
+
+ // Now confirm we have 2 submenus (the one we added, plus "other")
+ int subs = 0;
+ for (item in [menu itemArray]) {
+ if ([item hasSubmenu])
+ subs++;
+ }
+ EXPECT_EQ(2, subs);
+
+ for (item in [menu itemArray]) {
+ if ([[item title] hasPrefix:@"http://super-duper"]) {
+ long_item = item;
+ break;
+ }
+ }
+ EXPECT_TRUE(long_item);
+
+ // Make sure a short title looks fine
+ NSString* s = [short_item title];
+ EXPECT_NSEQ([NSString stringWithUTF8String:short_url], s);
+
+ // Make sure a super-long title gets trimmed
+ s = [long_item title];
+ EXPECT_TRUE([s length] < strlen(long_url));
+
+ // Confirm tooltips and confirm they are not trimmed (like the item
+ // name might be). Add tolerance for URL fixer-upping;
+ // e.g. http://foo becomes http://foo/)
+ EXPECT_GE([[short_item toolTip] length], (2*strlen(short_url) - 5));
+ EXPECT_GE([[long_item toolTip] length], (2*strlen(long_url) - 5));
+
+ // Make sure the favicon is non-nil (should be either the default site
+ // icon or a favicon, if present).
+ EXPECT_TRUE([short_item image]);
+ EXPECT_TRUE([long_item image]);
+}
+
+// Makes sure our internal map of BookmarkNode to NSMenuItem works.
+TEST_F(BookmarkMenuBridgeTest, TestGetMenuItemForNode) {
+ string16 empty;
+ NSMenu* menu = bridge_->menu_.get();
+
+ BookmarkModel* model = bridge_->GetBookmarkModel();
+ const BookmarkNode* bookmark_bar = model->GetBookmarkBarNode();
+ const BookmarkNode* root = model->AddGroup(bookmark_bar, 0, empty);
+ EXPECT_TRUE(model && root);
+
+ model->AddURL(root, 0, ASCIIToUTF16("Test Item"), GURL("http://test"));
+ AddNodeToMenu(bridge_.get(), root, menu);
+ EXPECT_TRUE(MenuItemForNode(bridge_.get(), root->GetChild(0)));
+
+ model->AddURL(root, 1, ASCIIToUTF16("Test 2"), GURL("http://second-test"));
+ AddNodeToMenu(bridge_.get(), root, menu);
+ EXPECT_TRUE(MenuItemForNode(bridge_.get(), root->GetChild(0)));
+ EXPECT_TRUE(MenuItemForNode(bridge_.get(), root->GetChild(1)));
+
+ const BookmarkNode* removed_node = root->GetChild(0);
+ EXPECT_EQ(2, root->GetChildCount());
+ model->Remove(root, 0);
+ EXPECT_EQ(1, root->GetChildCount());
+ bridge_->UpdateMenu(menu);
+ EXPECT_FALSE(MenuItemForNode(bridge_.get(), removed_node));
+ EXPECT_TRUE(MenuItemForNode(bridge_.get(), root->GetChild(0)));
+
+ const BookmarkNode empty_node(GURL("http://no-where/"));
+ EXPECT_FALSE(MenuItemForNode(bridge_.get(), &empty_node));
+ EXPECT_FALSE(MenuItemForNode(bridge_.get(), NULL));
+}
+
+// Test that Loaded() adds both the bookmark bar nodes and the "other" nodes.
+TEST_F(BookmarkMenuBridgeTest, TestAddNodeToOther) {
+ NSMenu* menu = bridge_->menu_.get();
+
+ BookmarkModel* model = bridge_->GetBookmarkModel();
+ const BookmarkNode* root = model->other_node();
+ EXPECT_TRUE(model && root);
+
+ const char* short_url = "http://foo/";
+ model->AddURL(root, 0, ASCIIToUTF16(short_url), GURL(short_url));
+
+ bridge_->UpdateMenu(menu);
+ ASSERT_GT([menu numberOfItems], 0);
+ NSMenuItem* other = [menu itemAtIndex:([menu numberOfItems]-1)];
+ EXPECT_TRUE(other);
+ EXPECT_TRUE([other hasSubmenu]);
+ ASSERT_GT([[other submenu] numberOfItems], 0);
+ EXPECT_NSEQ(@"http://foo/", [[[other submenu] itemAtIndex:0] title]);
+}
+
+TEST_F(BookmarkMenuBridgeTest, TestFavIconLoading) {
+ NSMenu* menu = bridge_->menu_;
+
+ BookmarkModel* model = bridge_->GetBookmarkModel();
+ const BookmarkNode* root = model->GetBookmarkBarNode();
+ EXPECT_TRUE(model && root);
+
+ const BookmarkNode* node =
+ model->AddURL(root, 0, ASCIIToUTF16("Test Item"),
+ GURL("http://favicon-test"));
+ bridge_->UpdateMenu(menu);
+ NSMenuItem* item = [menu itemWithTitle:@"Test Item"];
+ EXPECT_TRUE([item image]);
+ [item setImage:nil];
+ bridge_->BookmarkNodeFavIconLoaded(model, node);
+ EXPECT_TRUE([item image]);
+}
+
+TEST_F(BookmarkMenuBridgeTest, TestChangeTitle) {
+ NSMenu* menu = bridge_->menu_;
+ BookmarkModel* model = bridge_->GetBookmarkModel();
+ const BookmarkNode* root = model->GetBookmarkBarNode();
+ EXPECT_TRUE(model && root);
+
+ const BookmarkNode* node =
+ model->AddURL(root, 0, ASCIIToUTF16("Test Item"),
+ GURL("http://title-test"));
+ bridge_->UpdateMenu(menu);
+ NSMenuItem* item = [menu itemWithTitle:@"Test Item"];
+ EXPECT_TRUE([item image]);
+
+ model->SetTitle(node, ASCIIToUTF16("New Title"));
+
+ item = [menu itemWithTitle:@"Test Item"];
+ EXPECT_FALSE(item);
+ item = [menu itemWithTitle:@"New Title"];
+ EXPECT_TRUE(item);
+}
+
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h
new file mode 100644
index 0000000..66ecc45
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h
@@ -0,0 +1,46 @@
+// 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.
+
+// Controller (MVC) for the bookmark menu.
+// All bookmark menu item commands get directed here.
+// Unfortunately there is already a C++ class named BookmarkMenuController.
+
+#ifndef CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MENU_COCOA_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MENU_COCOA_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/cocoa_protocols_mac.h"
+
+class BookmarkNode;
+class BookmarkMenuBridge;
+
+@interface BookmarkMenuCocoaController : NSObject<NSMenuDelegate> {
+ @private
+ BookmarkMenuBridge* bridge_; // weak; owns me
+}
+
+// The Bookmarks menu
+@property (nonatomic, readonly) NSMenu* menu;
+
+// Return an autoreleased string to be used as a menu title for the
+// given bookmark node.
++ (NSString*)menuTitleForNode:(const BookmarkNode*)node;
+
+- (id)initWithBridge:(BookmarkMenuBridge *)bridge;
+
+// Called by any Bookmark menu item.
+// The menu item's tag is the bookmark ID.
+- (IBAction)openBookmarkMenuItem:(id)sender;
+
+@end // BookmarkMenuCocoaController
+
+
+@interface BookmarkMenuCocoaController (ExposedForUnitTests)
+- (const BookmarkNode*)nodeForIdentifier:(int)identifier;
+- (void)openURLForNode:(const BookmarkNode*)node;
+@end // BookmarkMenuCocoaController (ExposedForUnitTests)
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MENU_COCOA_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.mm
new file mode 100644
index 0000000..4c36120
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.mm
@@ -0,0 +1,98 @@
+// 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/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
+
+#include "app/text_elider.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h" // IDC_BOOKMARK_MENU
+#import "chrome/browser/app_controller_mac.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/ui/browser.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h"
+#include "chrome/browser/ui/cocoa/event_utils.h"
+#include "webkit/glue/window_open_disposition.h"
+
+namespace {
+
+// Menus more than this many pixels wide will get trimmed
+// TODO(jrg): ask UI dudes what a good value is.
+const NSUInteger kMaximumMenuPixelsWide = 300;
+
+}
+
+@implementation BookmarkMenuCocoaController
+
++ (NSString*)menuTitleForNode:(const BookmarkNode*)node {
+ NSFont* nsfont = [NSFont menuBarFontOfSize:0]; // 0 means "default"
+ gfx::Font font(base::SysNSStringToWide([nsfont fontName]),
+ static_cast<int>([nsfont pointSize]));
+ string16 title = gfx::ElideText(node->GetTitle(),
+ font,
+ kMaximumMenuPixelsWide,
+ false);
+ return base::SysUTF16ToNSString(title);
+}
+
+- (id)initWithBridge:(BookmarkMenuBridge *)bridge {
+ if ((self = [super init])) {
+ bridge_ = bridge;
+ DCHECK(bridge_);
+ [[self menu] setDelegate:self];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [[self menu] setDelegate:nil];
+ [super dealloc];
+}
+
+- (NSMenu*)menu {
+ return [[[NSApp mainMenu] itemWithTag:IDC_BOOKMARK_MENU] submenu];
+}
+
+- (BOOL)validateMenuItem:(NSMenuItem*)menuItem {
+ AppController* controller = [NSApp delegate];
+ return [controller keyWindowIsNotModal];
+}
+
+// NSMenu delegate method: called just before menu is displayed.
+- (void)menuNeedsUpdate:(NSMenu*)menu {
+ bridge_->UpdateMenu(menu);
+}
+
+// Return the a BookmarkNode that has the given id (called
+// "identifier" here to avoid conflict with objc's concept of "id").
+- (const BookmarkNode*)nodeForIdentifier:(int)identifier {
+ return bridge_->GetBookmarkModel()->GetNodeByID(identifier);
+}
+
+// Open the URL of the given BookmarkNode in the current tab.
+- (void)openURLForNode:(const BookmarkNode*)node {
+ Browser* browser = Browser::GetTabbedBrowser(bridge_->GetProfile(), true);
+ if (!browser)
+ browser = Browser::Create(bridge_->GetProfile());
+ WindowOpenDisposition disposition =
+ event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
+ browser->OpenURL(node->GetURL(), GURL(), disposition,
+ PageTransition::AUTO_BOOKMARK);
+}
+
+- (IBAction)openBookmarkMenuItem:(id)sender {
+ NSInteger tag = [sender tag];
+ int identifier = tag;
+ const BookmarkNode* node = [self nodeForIdentifier:identifier];
+ DCHECK(node);
+ if (!node)
+ return; // shouldn't be reached
+
+ [self openURLForNode:node];
+}
+
+@end // BookmarkMenuCocoaController
+
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller_unittest.mm
new file mode 100644
index 0000000..22930bc
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller_unittest.mm
@@ -0,0 +1,66 @@
+// 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/string16.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
+#include "chrome/browser/ui/browser.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+@interface FakeBookmarkMenuController : BookmarkMenuCocoaController {
+ @public
+ BrowserTestHelper* helper_;
+ const BookmarkNode* nodes_[2];
+ BOOL opened_[2];
+}
+@end
+
+@implementation FakeBookmarkMenuController
+
+- (id)init {
+ if ((self = [super init])) {
+ string16 empty;
+ helper_ = new BrowserTestHelper();
+ BookmarkModel* model = helper_->browser()->profile()->GetBookmarkModel();
+ const BookmarkNode* bookmark_bar = model->GetBookmarkBarNode();
+ nodes_[0] = model->AddURL(bookmark_bar, 0, empty, GURL("http://0.com"));
+ nodes_[1] = model->AddURL(bookmark_bar, 1, empty, GURL("http://1.com"));
+ }
+ return self;
+}
+
+- (void)dealloc {
+ delete helper_;
+ [super dealloc];
+}
+
+- (const BookmarkNode*)nodeForIdentifier:(int)identifier {
+ if ((identifier < 0) || (identifier >= 2))
+ return NULL;
+ return nodes_[identifier];
+}
+
+- (void)openURLForNode:(const BookmarkNode*)node {
+ std::string url = node->GetURL().possibly_invalid_spec();
+ if (url.find("http://0.com") != std::string::npos)
+ opened_[0] = YES;
+ if (url.find("http://1.com") != std::string::npos)
+ opened_[1] = YES;
+}
+
+@end // FakeBookmarkMenuController
+
+
+TEST(BookmarkMenuCocoaControllerTest, TestOpenItem) {
+ FakeBookmarkMenuController *c = [[FakeBookmarkMenuController alloc] init];
+ NSMenuItem *item = [[[NSMenuItem alloc] init] autorelease];
+ for (int i = 0; i < 2; i++) {
+ [item setTag:i];
+ ASSERT_EQ(c->opened_[i], NO);
+ [c openBookmarkMenuItem:item];
+ ASSERT_NE(c->opened_[i], NO);
+ }
+ [c release];
+}
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_unittest.mm
new file mode 100644
index 0000000..ef251b0
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_unittest.mm
@@ -0,0 +1,29 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class BookmarkMenuTest : public CocoaTest {
+};
+
+TEST_F(BookmarkMenuTest, Basics) {
+ scoped_nsobject<BookmarkMenu> menu([[BookmarkMenu alloc]
+ initWithTitle:@"title"]);
+ scoped_nsobject<NSMenuItem> item([[NSMenuItem alloc] initWithTitle:@"item"
+ action:NULL
+ keyEquivalent:@""]);
+ [menu addItem:item];
+ long long l = 103849459459598948LL; // arbitrary
+ NSNumber* number = [NSNumber numberWithLongLong:l];
+ [menu setRepresentedObject:number];
+ EXPECT_EQ(l, [menu id]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa.h
new file mode 100644
index 0000000..0a7da34
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa.h
@@ -0,0 +1,116 @@
+// 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.
+
+// C++ bridge class to send a selector to a Cocoa object when the
+// bookmark model changes. Some Cocoa objects edit the bookmark model
+// and temporarily save a copy of the state (e.g. bookmark button
+// editor). As a fail-safe, these objects want an easy cancel if the
+// model changes out from under them. For example, if you have the
+// bookmark button editor sheet open, then edit the bookmark in the
+// bookmark manager, we'd want to simply cancel the editor.
+//
+// This class is conservative and may result in notifications which
+// aren't strictly necessary. For example, node removal only needs to
+// cancel an edit if the removed node is a folder (editors often have
+// a list of "new parents"). But, just to be sure, notification
+// happens on any removal.
+
+#ifndef CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MODEL_OBSERVER_FOR_COCOA_H
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MODEL_OBSERVER_FOR_COCOA_H
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/basictypes.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/bookmarks/bookmark_model_observer.h"
+
+class BookmarkModelObserverForCocoa : public BookmarkModelObserver {
+ public:
+ // When |node| in |model| changes, send |selector| to |object|.
+ // Assumes |selector| is a selector that takes one arg, like an
+ // IBOutlet. The arg passed is nil.
+ // Many notifications happen independently of node
+ // (e.g. BeingDeleted), so |node| can be nil.
+ //
+ // |object| is NOT retained, since the expected use case is for
+ // ||object| to own the BookmarkModelObserverForCocoa and we don't
+ // want a retain cycle.
+ BookmarkModelObserverForCocoa(const BookmarkNode* node,
+ BookmarkModel* model,
+ NSObject* object,
+ SEL selector) {
+ DCHECK(model);
+ node_ = node;
+ model_ = model;
+ object_ = object;
+ selector_ = selector;
+ model_->AddObserver(this);
+ }
+ virtual ~BookmarkModelObserverForCocoa() {
+ model_->RemoveObserver(this);
+ }
+
+ virtual void BookmarkModelBeingDeleted(BookmarkModel* model) {
+ Notify();
+ }
+ virtual void BookmarkNodeMoved(BookmarkModel* model,
+ const BookmarkNode* old_parent,
+ int old_index,
+ const BookmarkNode* new_parent,
+ int new_index) {
+ // Editors often have a tree of parents, so movement of folders
+ // must cause a cancel.
+ Notify();
+ }
+ virtual void BookmarkNodeRemoved(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int old_index,
+ const BookmarkNode* node) {
+ // See comment in BookmarkNodeMoved.
+ Notify();
+ }
+ virtual void BookmarkNodeChanged(BookmarkModel* model,
+ const BookmarkNode* node) {
+ if ((node_ == node) || (!node_))
+ Notify();
+ }
+ virtual void BookmarkImportBeginning(BookmarkModel* model) {
+ // Be conservative.
+ Notify();
+ }
+
+ // Some notifications we don't care about, but by being pure virtual
+ // in the base class we must implement them.
+ virtual void Loaded(BookmarkModel* model) {
+ }
+ virtual void BookmarkNodeAdded(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int index) {
+ }
+ virtual void BookmarkNodeFavIconLoaded(BookmarkModel* model,
+ const BookmarkNode* node) {
+ }
+ virtual void BookmarkNodeChildrenReordered(BookmarkModel* model,
+ const BookmarkNode* node) {
+ }
+
+ virtual void BookmarkImportEnding(BookmarkModel* model) {
+ }
+
+ private:
+ const BookmarkNode* node_; // Weak; owned by a BookmarkModel.
+ BookmarkModel* model_; // Weak; it is owned by a Profile.
+ NSObject* object_; // Weak, like a delegate.
+ SEL selector_;
+
+ void Notify() {
+ [object_ performSelector:selector_ withObject:nil];
+ }
+
+ DISALLOW_COPY_AND_ASSIGN(BookmarkModelObserverForCocoa);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MODEL_OBSERVER_FOR_COCOA_H
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa_unittest.mm
new file mode 100644
index 0000000..5ff8687
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa_unittest.mm
@@ -0,0 +1,68 @@
+// 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_ptr.h"
+#include "base/scoped_nsobject.h"
+#include "base/utf_string_conversions.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa.h"
+#import "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+
+// Keep track of bookmark pings.
+@interface ObserverPingTracker : NSObject {
+ @public
+ int pings;
+}
+@end
+
+@implementation ObserverPingTracker
+- (void)pingMe:(id)sender {
+ pings++;
+}
+@end
+
+namespace {
+
+class BookmarkModelObserverForCocoaTest : public CocoaTest {
+ public:
+ BrowserTestHelper helper_;
+
+ BookmarkModelObserverForCocoaTest() {}
+ virtual ~BookmarkModelObserverForCocoaTest() {}
+ private:
+ DISALLOW_COPY_AND_ASSIGN(BookmarkModelObserverForCocoaTest);
+};
+
+
+TEST_F(BookmarkModelObserverForCocoaTest, TestCallback) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(),
+ 0, ASCIIToUTF16("super"),
+ GURL("http://www.google.com"));
+
+ scoped_nsobject<ObserverPingTracker>
+ pingCount([[ObserverPingTracker alloc] init]);
+
+ scoped_ptr<BookmarkModelObserverForCocoa>
+ observer(new BookmarkModelObserverForCocoa(node, model,
+ pingCount,
+ @selector(pingMe:)));
+
+ EXPECT_EQ(0, pingCount.get()->pings);
+
+ model->SetTitle(node, ASCIIToUTF16("duper"));
+ EXPECT_EQ(1, pingCount.get()->pings);
+ model->SetURL(node, GURL("http://www.google.com/reader"));
+ EXPECT_EQ(2, pingCount.get()->pings);
+
+ model->Move(node, model->other_node(), 0);
+ EXPECT_EQ(3, pingCount.get()->pings);
+
+ model->Remove(node->GetParent(), 0);
+ EXPECT_EQ(4, pingCount.get()->pings);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h
new file mode 100644
index 0000000..40f1cb1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h
@@ -0,0 +1,64 @@
+// 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_UI_COCOA_BOOKMARKS_BOOKMARK_NAME_FOLDER_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_NAME_FOLDER_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+
+class BookmarkModelObserverForCocoa;
+
+// A controller for dialog to let the user create a new folder or
+// rename an existing folder. Accessible from a context menu on a
+// bookmark button or the bookmark bar.
+@interface BookmarkNameFolderController : NSWindowController {
+ @private
+ IBOutlet NSTextField* nameField_;
+ IBOutlet NSButton* okButton_;
+
+ NSWindow* parentWindow_; // weak
+ Profile* profile_; // weak
+
+ // Weak; owned by the model. Can be NULL (see below). Either node_
+ // is non-NULL (renaming a folder), or parent_ is non-NULL (adding a
+ // new one).
+ const BookmarkNode* node_;
+ const BookmarkNode* parent_;
+ int newIndex_;
+
+ scoped_nsobject<NSString> initialName_;
+
+ // Ping me when things change out from under us.
+ scoped_ptr<BookmarkModelObserverForCocoa> observer_;
+}
+
+// Use the 1st initializer for a "rename existing folder" request.
+//
+// Use the 2nd initializer for an "add folder" request. If creating a
+// new folder |parent| and |newIndex| specify where to put the new
+// node.
+- (id)initWithParentWindow:(NSWindow*)window
+ profile:(Profile*)profile
+ node:(const BookmarkNode*)node;
+- (id)initWithParentWindow:(NSWindow*)window
+ profile:(Profile*)profile
+ parent:(const BookmarkNode*)parent
+ newIndex:(int)newIndex;
+- (void)runAsModalSheet;
+- (IBAction)cancel:(id)sender;
+- (IBAction)ok:(id)sender;
+@end
+
+@interface BookmarkNameFolderController(TestingAPI)
+- (NSString*)folderName;
+- (void)setFolderName:(NSString*)name;
+- (NSButton*)okButton;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_NAME_FOLDER_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.mm
new file mode 100644
index 0000000..8c34af2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.mm
@@ -0,0 +1,123 @@
+// 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/ui/cocoa/bookmarks/bookmark_name_folder_controller.h"
+
+#include "app/l10n_util.h"
+#include "app/l10n_util_mac.h"
+#include "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa.h"
+#include "grit/generated_resources.h"
+
+@implementation BookmarkNameFolderController
+
+// Common initializer (private).
+- (id)initWithParentWindow:(NSWindow*)window
+ profile:(Profile*)profile
+ node:(const BookmarkNode*)node
+ parent:(const BookmarkNode*)parent
+ newIndex:(int)newIndex {
+ NSString* nibpath = [mac_util::MainAppBundle()
+ pathForResource:@"BookmarkNameFolder"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ parentWindow_ = window;
+ profile_ = profile;
+ node_ = node;
+ parent_ = parent;
+ newIndex_ = newIndex;
+ if (parent) {
+ DCHECK_LE(newIndex, parent->GetChildCount());
+ }
+ if (node_) {
+ initialName_.reset([base::SysUTF16ToNSString(node_->GetTitle()) retain]);
+ } else {
+ NSString* newString =
+ l10n_util::GetNSStringWithFixup(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME);
+ initialName_.reset([newString retain]);
+ }
+ }
+ return self;
+}
+
+- (id)initWithParentWindow:(NSWindow*)window
+ profile:(Profile*)profile
+ node:(const BookmarkNode*)node {
+ DCHECK(node);
+ return [self initWithParentWindow:window
+ profile:profile
+ node:node
+ parent:nil
+ newIndex:0];
+}
+
+- (id)initWithParentWindow:(NSWindow*)window
+ profile:(Profile*)profile
+ parent:(const BookmarkNode*)parent
+ newIndex:(int)newIndex {
+ DCHECK(parent);
+ return [self initWithParentWindow:window
+ profile:profile
+ node:nil
+ parent:parent
+ newIndex:newIndex];
+}
+
+- (void)awakeFromNib {
+ [nameField_ setStringValue:initialName_.get()];
+}
+
+- (void)runAsModalSheet {
+ // Ping me when things change out from under us.
+ observer_.reset(new BookmarkModelObserverForCocoa(
+ node_, profile_->GetBookmarkModel(),
+ self,
+ @selector(cancel:)));
+ [NSApp beginSheet:[self window]
+ modalForWindow:parentWindow_
+ modalDelegate:self
+ didEndSelector:@selector(didEndSheet:returnCode:contextInfo:)
+ contextInfo:nil];
+}
+
+- (IBAction)cancel:(id)sender {
+ [NSApp endSheet:[self window]];
+}
+
+- (IBAction)ok:(id)sender {
+ NSString* name = [nameField_ stringValue];
+ BookmarkModel* model = profile_->GetBookmarkModel();
+ if (node_) {
+ model->SetTitle(node_, base::SysNSStringToUTF16(name));
+ } else {
+ model->AddGroup(parent_,
+ newIndex_,
+ base::SysNSStringToUTF16(name));
+ }
+ [NSApp endSheet:[self window]];
+}
+
+- (void)didEndSheet:(NSWindow*)sheet
+ returnCode:(int)returnCode
+ contextInfo:(void*)contextInfo {
+ [[self window] orderOut:self];
+ observer_.reset(NULL);
+ [self autorelease];
+}
+
+- (NSString*)folderName {
+ return [nameField_ stringValue];
+}
+
+- (void)setFolderName:(NSString*)name {
+ [nameField_ setStringValue:name];
+}
+
+- (NSButton*)okButton {
+ return okButton_;
+}
+
+@end // BookmarkNameFolderController
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller_unittest.mm
new file mode 100644
index 0000000..69fb939
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller_unittest.mm
@@ -0,0 +1,172 @@
+// 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"
+#include "base/utf_string_conversions.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+class BookmarkNameFolderControllerTest : public CocoaTest {
+ public:
+ BrowserTestHelper helper_;
+};
+
+
+// Simple add of a node (at the end).
+TEST_F(BookmarkNameFolderControllerTest, AddNew) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ EXPECT_EQ(0, parent->GetChildCount());
+
+ scoped_nsobject<BookmarkNameFolderController>
+ controller([[BookmarkNameFolderController alloc]
+ initWithParentWindow:test_window()
+ profile:helper_.profile()
+ parent:parent
+ newIndex:0]);
+ [controller window]; // force nib load
+
+ // Do nothing.
+ [controller cancel:nil];
+ EXPECT_EQ(0, parent->GetChildCount());
+
+ // Change name then cancel.
+ [controller setFolderName:@"Bozo"];
+ [controller cancel:nil];
+ EXPECT_EQ(0, parent->GetChildCount());
+
+ // Add a new folder.
+ [controller ok:nil];
+ EXPECT_EQ(1, parent->GetChildCount());
+ EXPECT_TRUE(parent->GetChild(0)->is_folder());
+ EXPECT_EQ(ASCIIToUTF16("Bozo"), parent->GetChild(0)->GetTitle());
+}
+
+// Add new but specify a sibling.
+TEST_F(BookmarkNameFolderControllerTest, AddNewWithSibling) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+
+ // Add 2 nodes. We will place the new folder in the middle of these.
+ model->AddURL(parent, 0, ASCIIToUTF16("title 1"),
+ GURL("http://www.google.com"));
+ model->AddURL(parent, 1, ASCIIToUTF16("title 3"),
+ GURL("http://www.google.com"));
+ EXPECT_EQ(2, parent->GetChildCount());
+
+ scoped_nsobject<BookmarkNameFolderController>
+ controller([[BookmarkNameFolderController alloc]
+ initWithParentWindow:test_window()
+ profile:helper_.profile()
+ parent:parent
+ newIndex:1]);
+ [controller window]; // force nib load
+
+ // Add a new folder.
+ [controller setFolderName:@"middle"];
+ [controller ok:nil];
+
+ // Confirm we now have 3, and that the new one is in the middle.
+ EXPECT_EQ(3, parent->GetChildCount());
+ EXPECT_TRUE(parent->GetChild(1)->is_folder());
+ EXPECT_EQ(ASCIIToUTF16("middle"), parent->GetChild(1)->GetTitle());
+}
+
+// Make sure we are allowed to create a folder named "New Folder".
+TEST_F(BookmarkNameFolderControllerTest, AddNewDefaultName) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ EXPECT_EQ(0, parent->GetChildCount());
+
+ scoped_nsobject<BookmarkNameFolderController>
+ controller([[BookmarkNameFolderController alloc]
+ initWithParentWindow:test_window()
+ profile:helper_.profile()
+ parent:parent
+ newIndex:0]);
+
+ [controller window]; // force nib load
+
+ // Click OK without changing the name
+ [controller ok:nil];
+ EXPECT_EQ(1, parent->GetChildCount());
+ EXPECT_TRUE(parent->GetChild(0)->is_folder());
+}
+
+// Make sure we are allowed to create a folder with an empty name.
+TEST_F(BookmarkNameFolderControllerTest, AddNewBlankName) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ EXPECT_EQ(0, parent->GetChildCount());
+
+ scoped_nsobject<BookmarkNameFolderController>
+ controller([[BookmarkNameFolderController alloc]
+ initWithParentWindow:test_window()
+ profile:helper_.profile()
+ parent:parent
+ newIndex:0]);
+ [controller window]; // force nib load
+
+ // Change the name to blank, click OK.
+ [controller setFolderName:@""];
+ [controller ok:nil];
+ EXPECT_EQ(1, parent->GetChildCount());
+ EXPECT_TRUE(parent->GetChild(0)->is_folder());
+}
+
+TEST_F(BookmarkNameFolderControllerTest, Rename) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ const BookmarkNode* folder = model->AddGroup(parent,
+ parent->GetChildCount(),
+ ASCIIToUTF16("group"));
+
+ // Rename the folder by creating a controller that originates from
+ // the node.
+ scoped_nsobject<BookmarkNameFolderController>
+ controller([[BookmarkNameFolderController alloc]
+ initWithParentWindow:test_window()
+ profile:helper_.profile()
+ node:folder]);
+ [controller window]; // force nib load
+
+ EXPECT_NSEQ(@"group", [controller folderName]);
+ [controller setFolderName:@"Zobo"];
+ [controller ok:nil];
+ EXPECT_EQ(1, parent->GetChildCount());
+ EXPECT_TRUE(parent->GetChild(0)->is_folder());
+ EXPECT_EQ(ASCIIToUTF16("Zobo"), parent->GetChild(0)->GetTitle());
+}
+
+TEST_F(BookmarkNameFolderControllerTest, EditAndConfirmOKButton) {
+ BookmarkModel* model = helper_.profile()->GetBookmarkModel();
+ const BookmarkNode* parent = model->GetBookmarkBarNode();
+ EXPECT_EQ(0, parent->GetChildCount());
+
+ scoped_nsobject<BookmarkNameFolderController>
+ controller([[BookmarkNameFolderController alloc]
+ initWithParentWindow:test_window()
+ profile:helper_.profile()
+ parent:parent
+ newIndex:0]);
+ [controller window]; // force nib load
+
+ // We start enabled since the default "New Folder" is added for us.
+ EXPECT_TRUE([[controller okButton] isEnabled]);
+
+ [controller setFolderName:@"Bozo"];
+ EXPECT_TRUE([[controller okButton] isEnabled]);
+ [controller setFolderName:@" "];
+ EXPECT_TRUE([[controller okButton] isEnabled]);
+
+ [controller setFolderName:@""];
+ EXPECT_TRUE([[controller okButton] isEnabled]);
+}
+
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h
new file mode 100644
index 0000000..08b195d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h
@@ -0,0 +1,35 @@
+// 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_UI_COCOA_BOOKMARKS_BOOKMARK_TREE_BROWSER_CELL_H_
+#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_TREE_BROWSER_CELL_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+class BookmarkNode;
+
+// Provides a custom cell as used in the BookmarkEditor.xib's folder tree
+// browser view. This cell customization adds target and action support
+// not provided by the NSBrowserCell as well as contextual information
+// identifying the bookmark node being edited and the column matrix
+// control in which is contained the cell.
+@interface BookmarkTreeBrowserCell : NSBrowserCell {
+ @private
+ const BookmarkNode* bookmarkNode_; // weak
+ NSMatrix* matrix_; // weak
+ id target_; // weak
+ SEL action_;
+}
+
+@property (nonatomic, assign) NSMatrix* matrix;
+@property (nonatomic, assign) id target;
+@property (nonatomic, assign) SEL action;
+
+- (const BookmarkNode*)bookmarkNode;
+- (void)setBookmarkNode:(const BookmarkNode*)bookmarkNode;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_TREE_BROWSER_CELL_H_
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.mm
new file mode 100644
index 0000000..6fe7e6e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.mm
@@ -0,0 +1,23 @@
+// 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/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h"
+
+#include "chrome/browser/bookmarks/bookmark_model.h"
+
+@implementation BookmarkTreeBrowserCell
+
+@synthesize matrix = matrix_;
+@synthesize target = target_;
+@synthesize action = action_;
+
+- (const BookmarkNode*)bookmarkNode {
+ return bookmarkNode_;
+}
+
+- (void)setBookmarkNode:(const BookmarkNode*)bookmarkNode {
+ bookmarkNode_ = bookmarkNode;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell_unittest.mm
new file mode 100644
index 0000000..5171018
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell_unittest.mm
@@ -0,0 +1,43 @@
+// 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 "chrome/browser/bookmarks/bookmark_model.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/platform_test.h"
+
+class BookmarkTreeBrowserCellTest : public PlatformTest {
+ public:
+ BookmarkTreeBrowserCellTest() {
+ // Set up our mocks.
+ GURL gurl;
+ bookmarkNodeMock_.reset(new BookmarkNode(gurl));
+ matrixMock_.reset([[NSMatrix alloc] init]);
+ targetMock_.reset([[NSObject alloc] init]);
+ }
+
+ scoped_ptr<BookmarkNode> bookmarkNodeMock_;
+ scoped_nsobject<NSMatrix> matrixMock_;
+ scoped_nsobject<NSObject> targetMock_;
+};
+
+TEST_F(BookmarkTreeBrowserCellTest, BasicAllocDealloc) {
+ BookmarkTreeBrowserCell* cell = [[[BookmarkTreeBrowserCell alloc]
+ initTextCell:@"TEST STRING"] autorelease];
+ [cell setMatrix:matrixMock_.get()];
+ [cell setTarget:targetMock_.get()];
+ [cell setAction:@selector(mockAction:)];
+ [cell setBookmarkNode:bookmarkNodeMock_.get()];
+
+ NSMatrix* testMatrix = [cell matrix];
+ EXPECT_EQ(testMatrix, matrixMock_.get());
+ id testTarget = [cell target];
+ EXPECT_EQ(testTarget, targetMock_.get());
+ SEL testAction = [cell action];
+ EXPECT_EQ(testAction, @selector(mockAction:));
+ const BookmarkNode* testBookmarkNode = [cell bookmarkNode];
+ EXPECT_EQ(testBookmarkNode, bookmarkNodeMock_.get());
+}
diff --git a/chrome/browser/ui/cocoa/browser_command_executor.h b/chrome/browser/ui/cocoa/browser_command_executor.h
new file mode 100644
index 0000000..e6e01cf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/browser_command_executor.h
@@ -0,0 +1,16 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_BROWSER_COMMAND_EXECUTOR_H_
+#define CHROME_BROWSER_BROWSER_COMMAND_EXECUTOR_H_
+#pragma once
+
+// Defines a protocol for any object that can execute commands in the
+// context of some underlying browser object.
+@protocol BrowserCommandExecutor
+- (void)executeCommand:(int)command;
+@end
+
+#endif // CHROME_BROWSER_BROWSER_COMMAND_EXECUTOR_H_
+
diff --git a/chrome/browser/ui/cocoa/browser_frame_view.h b/chrome/browser/ui/cocoa/browser_frame_view.h
new file mode 100644
index 0000000..42faa9b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/browser_frame_view.h
@@ -0,0 +1,65 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+// BrowserFrameView is a class whose methods we swizzle into NSGrayFrame
+// (an AppKit framework class) so that we can support custom frame drawing, and
+// have the ability to move our window widgets (close, zoom, miniaturize) where
+// we want them.
+// This class is never to be instantiated on its own.
+// We explored a variety of ways to support custom frame drawing and custom
+// window widgets.
+// Our requirements were:
+// a) that we could fall back on standard system drawing at any time for the
+// "default theme"
+// b) We needed to be able to draw both a background pattern, and an overlay
+// graphic, and we need to be able to set the pattern phase of our background
+// window.
+// c) We had to be able to support "transparent" themes, so that you could see
+// through to the underlying windows in places without the system theme
+// getting in the way.
+// d) We had to support having the custom window controls moved down by a couple
+// of pixels and rollovers, accessibility, etc. all had to work.
+//
+// Since we want "A" we couldn't just do a transparent borderless window. At
+// least I couldn't find the right combination of HITheme calls to make it draw
+// nicely, and I don't trust that the HITheme calls are going to exist in future
+// system versions.
+// "C" precluded us from inserting a view between the system frame and the
+// the content frame in Z order. To get the transparency we actually need to
+// replace the drawing of the system frame.
+// "D" required us to override _mouseInGroup to get our custom widget rollovers
+// drawing correctly. The widgets call _mouseInGroup on their superview to
+// decide whether they should draw in highlight mode or not.
+// "B" precluded us from just setting a background color on the window.
+//
+// Originally we tried overriding the private API +frameViewForStyleMask: to
+// add our own subclass of NSGrayView to our window. Turns out that if you
+// subclass NSGrayView it does not draw correctly when you call NSGrayView's
+// drawRect. It appears that NSGrayView's drawRect: method (and that of its
+// superclasses) do lots of "isMemberOfClass/isKindOfClass" calls, and if your
+// class is NOT an instance of NSGrayView (as opposed to a subclass of
+// NSGrayView) then the system drawing will not work correctly.
+//
+// Given all of the above, we found swizzling drawRect, and adding an
+// implementation of _mouseInGroup and updateTrackingAreas, in _load to be the
+// easiest and safest method of achieving our goals. We do the best we can to
+// check that everything is safe, and attempt to fallback gracefully if it is
+// not.
+@interface BrowserFrameView : NSView
+
+// Draws the window theme into the specified rect. Returns whether a theme was
+// drawn (whether incognito or full pattern theme; an overlay image doesn't
+// count).
++ (BOOL)drawWindowThemeInDirtyRect:(NSRect)dirtyRect
+ forView:(NSView*)view
+ bounds:(NSRect)bounds
+ offset:(NSPoint)offset
+ forceBlackBackground:(BOOL)forceBlackBackground;
+
+// Gets the color to draw title text.
++ (NSColor*)titleColorForThemeView:(NSView*)view;
+
+@end
diff --git a/chrome/browser/ui/cocoa/browser_frame_view.mm b/chrome/browser/ui/cocoa/browser_frame_view.mm
new file mode 100644
index 0000000..a966785
--- /dev/null
+++ b/chrome/browser/ui/cocoa/browser_frame_view.mm
@@ -0,0 +1,399 @@
+// 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/ui/cocoa/browser_frame_view.h"
+
+#import <objc/runtime.h>
+#import <Carbon/Carbon.h>
+
+#include "base/logging.h"
+#include "base/mac/scoped_nsautorelease_pool.h"
+#import "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/framed_browser_window.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#include "grit/theme_resources.h"
+
+static const CGFloat kBrowserFrameViewPaintHeight = 60.0;
+static const NSPoint kBrowserFrameViewPatternPhaseOffset = { -5, 3 };
+
+static BOOL gCanDrawTitle = NO;
+static BOOL gCanGetCornerRadius = NO;
+
+@interface NSView (Swizzles)
+- (void)drawRectOriginal:(NSRect)rect;
+- (BOOL)_mouseInGroup:(NSButton*)widget;
+- (void)updateTrackingAreas;
+- (NSUInteger)_shadowFlagsOriginal;
+@end
+
+// Undocumented APIs. They are really on NSGrayFrame rather than
+// BrowserFrameView, but we call them from methods swizzled onto NSGrayFrame.
+@interface BrowserFrameView (UndocumentedAPI)
+
+- (float)roundedCornerRadius;
+- (CGRect)_titlebarTitleRect;
+- (void)_drawTitleStringIn:(struct CGRect)arg1 withColor:(id)color;
+- (NSUInteger)_shadowFlags;
+
+@end
+
+@implementation BrowserFrameView
+
++ (void)load {
+ // This is where we swizzle drawRect, and add in two methods that we
+ // need. If any of these fail it shouldn't affect the functionality of the
+ // others. If they all fail, we will lose window frame theming and
+ // roll overs for our close widgets, but things should still function
+ // correctly.
+ base::mac::ScopedNSAutoreleasePool pool;
+ Class grayFrameClass = NSClassFromString(@"NSGrayFrame");
+ DCHECK(grayFrameClass);
+ if (!grayFrameClass) return;
+
+ // Exchange draw rect.
+ Method m0 = class_getInstanceMethod([self class], @selector(drawRect:));
+ DCHECK(m0);
+ if (m0) {
+ BOOL didAdd = class_addMethod(grayFrameClass,
+ @selector(drawRectOriginal:),
+ method_getImplementation(m0),
+ method_getTypeEncoding(m0));
+ DCHECK(didAdd);
+ if (didAdd) {
+ Method m1 = class_getInstanceMethod(grayFrameClass, @selector(drawRect:));
+ Method m2 = class_getInstanceMethod(grayFrameClass,
+ @selector(drawRectOriginal:));
+ DCHECK(m1 && m2);
+ if (m1 && m2) {
+ method_exchangeImplementations(m1, m2);
+ }
+ }
+ }
+
+ // Add _mouseInGroup.
+ m0 = class_getInstanceMethod([self class], @selector(_mouseInGroup:));
+ DCHECK(m0);
+ if (m0) {
+ BOOL didAdd = class_addMethod(grayFrameClass,
+ @selector(_mouseInGroup:),
+ method_getImplementation(m0),
+ method_getTypeEncoding(m0));
+ DCHECK(didAdd);
+ }
+ // Add updateTrackingArea.
+ m0 = class_getInstanceMethod([self class], @selector(updateTrackingAreas));
+ DCHECK(m0);
+ if (m0) {
+ BOOL didAdd = class_addMethod(grayFrameClass,
+ @selector(updateTrackingAreas),
+ method_getImplementation(m0),
+ method_getTypeEncoding(m0));
+ DCHECK(didAdd);
+ }
+
+ gCanDrawTitle =
+ [grayFrameClass
+ instancesRespondToSelector:@selector(_titlebarTitleRect)] &&
+ [grayFrameClass
+ instancesRespondToSelector:@selector(_drawTitleStringIn:withColor:)];
+ gCanGetCornerRadius =
+ [grayFrameClass
+ instancesRespondToSelector:@selector(roundedCornerRadius)];
+
+ // Add _shadowFlags. This is a method on NSThemeFrame, not on NSGrayFrame.
+ // NSThemeFrame is NSGrayFrame's superclass.
+ Class themeFrameClass = NSClassFromString(@"NSThemeFrame");
+ DCHECK(themeFrameClass);
+ if (!themeFrameClass) return;
+ m0 = class_getInstanceMethod([self class], @selector(_shadowFlags));
+ DCHECK(m0);
+ if (m0) {
+ BOOL didAdd = class_addMethod(themeFrameClass,
+ @selector(_shadowFlagsOriginal),
+ method_getImplementation(m0),
+ method_getTypeEncoding(m0));
+ DCHECK(didAdd);
+ if (didAdd) {
+ Method m1 = class_getInstanceMethod(themeFrameClass,
+ @selector(_shadowFlags));
+ Method m2 = class_getInstanceMethod(themeFrameClass,
+ @selector(_shadowFlagsOriginal));
+ DCHECK(m1 && m2);
+ if (m1 && m2) {
+ method_exchangeImplementations(m1, m2);
+ }
+ }
+ }
+}
+
+- (id)initWithFrame:(NSRect)frame {
+ // This class is not for instantiating.
+ [self doesNotRecognizeSelector:_cmd];
+ return nil;
+}
+
+- (id)initWithCoder:(NSCoder*)coder {
+ // This class is not for instantiating.
+ [self doesNotRecognizeSelector:_cmd];
+ return nil;
+}
+
+// Here is our custom drawing for our frame.
+- (void)drawRect:(NSRect)rect {
+ // If this isn't the window class we expect, then pass it on to the
+ // original implementation.
+ if (![[self window] isKindOfClass:[FramedBrowserWindow class]]) {
+ [self drawRectOriginal:rect];
+ return;
+ }
+
+ // WARNING: There is an obvious optimization opportunity here that you DO NOT
+ // want to take. To save painting cycles, you might think it would be a good
+ // idea to call out to -drawRectOriginal: only if no theme were drawn. In
+ // reality, however, if you fail to call -drawRectOriginal:, or if you call it
+ // after a clipping path is set, the rounded corners at the top of the window
+ // will not draw properly. Do not try to be smart here.
+
+ // Only paint the top of the window.
+ NSWindow* window = [self window];
+ NSRect windowRect = [self convertRect:[window frame] fromView:nil];
+ windowRect.origin = NSMakePoint(0, 0);
+
+ NSRect paintRect = windowRect;
+ paintRect.origin.y = NSMaxY(paintRect) - kBrowserFrameViewPaintHeight;
+ paintRect.size.height = kBrowserFrameViewPaintHeight;
+ rect = NSIntersectionRect(paintRect, rect);
+ [self drawRectOriginal:rect];
+
+ // Set up our clip.
+ float cornerRadius = 4.0;
+ if (gCanGetCornerRadius)
+ cornerRadius = [self roundedCornerRadius];
+ [[NSBezierPath bezierPathWithRoundedRect:windowRect
+ xRadius:cornerRadius
+ yRadius:cornerRadius] addClip];
+ [[NSBezierPath bezierPathWithRect:rect] addClip];
+
+ // Do the theming.
+ BOOL themed = [BrowserFrameView drawWindowThemeInDirtyRect:rect
+ forView:self
+ bounds:windowRect
+ offset:NSZeroPoint
+ forceBlackBackground:NO];
+
+ // If the window needs a title and we painted over the title as drawn by the
+ // default window paint, paint it ourselves.
+ if (themed && gCanDrawTitle && ![[self window] _isTitleHidden]) {
+ [self _drawTitleStringIn:[self _titlebarTitleRect]
+ withColor:[BrowserFrameView titleColorForThemeView:self]];
+ }
+
+ // Pinstripe the top.
+ if (themed) {
+ NSSize windowPixel = [self convertSizeFromBase:NSMakeSize(1, 1)];
+
+ windowRect = [self convertRect:[window frame] fromView:nil];
+ windowRect.origin = NSMakePoint(0, 0);
+ windowRect.origin.y -= 0.5 * windowPixel.height;
+ windowRect.origin.x -= 0.5 * windowPixel.width;
+ windowRect.size.width += windowPixel.width;
+ [[NSColor colorWithCalibratedWhite:1.0 alpha:0.5] set];
+ NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:windowRect
+ xRadius:cornerRadius
+ yRadius:cornerRadius];
+ [path setLineWidth:windowPixel.width];
+ [path stroke];
+ }
+}
+
++ (BOOL)drawWindowThemeInDirtyRect:(NSRect)dirtyRect
+ forView:(NSView*)view
+ bounds:(NSRect)bounds
+ offset:(NSPoint)offset
+ forceBlackBackground:(BOOL)forceBlackBackground {
+ ThemeProvider* themeProvider = [[view window] themeProvider];
+ if (!themeProvider)
+ return NO;
+
+ ThemedWindowStyle windowStyle = [[view window] themedWindowStyle];
+
+ // Devtools windows don't get themed.
+ if (windowStyle & THEMED_DEVTOOLS)
+ return NO;
+
+ BOOL active = [[view window] isMainWindow];
+ BOOL incognito = windowStyle & THEMED_INCOGNITO;
+ BOOL popup = windowStyle & THEMED_POPUP;
+
+ // Find a theme image.
+ NSColor* themeImageColor = nil;
+ int themeImageID;
+ if (popup && active)
+ themeImageID = IDR_THEME_TOOLBAR;
+ else if (popup && !active)
+ themeImageID = IDR_THEME_TAB_BACKGROUND;
+ else if (!popup && active && incognito)
+ themeImageID = IDR_THEME_FRAME_INCOGNITO;
+ else if (!popup && active && !incognito)
+ themeImageID = IDR_THEME_FRAME;
+ else if (!popup && !active && incognito)
+ themeImageID = IDR_THEME_FRAME_INCOGNITO_INACTIVE;
+ else
+ themeImageID = IDR_THEME_FRAME_INACTIVE;
+ if (themeProvider->HasCustomImage(IDR_THEME_FRAME))
+ themeImageColor = themeProvider->GetNSImageColorNamed(themeImageID, true);
+
+ // If no theme image, use a gradient if incognito.
+ NSGradient* gradient = nil;
+ if (!themeImageColor && incognito)
+ gradient = themeProvider->GetNSGradient(
+ active ? BrowserThemeProvider::GRADIENT_FRAME_INCOGNITO :
+ BrowserThemeProvider::GRADIENT_FRAME_INCOGNITO_INACTIVE);
+
+ BOOL themed = NO;
+ if (themeImageColor) {
+ // The titlebar/tabstrip header on the mac is slightly smaller than on
+ // Windows. To keep the window background lined up with the tab and toolbar
+ // patterns, we have to shift the pattern slightly, rather than simply
+ // drawing it from the top left corner. The offset below was empirically
+ // determined in order to line these patterns up.
+ //
+ // This will make the themes look slightly different than in Windows/Linux
+ // because of the differing heights between window top and tab top, but this
+ // has been approved by UI.
+ NSView* frameView = [[[view window] contentView] superview];
+ NSPoint topLeft = NSMakePoint(NSMinX(bounds), NSMaxY(bounds));
+ NSPoint topLeftInFrameCoordinates =
+ [view convertPoint:topLeft toView:frameView];
+
+ NSPoint phase = kBrowserFrameViewPatternPhaseOffset;
+ phase.x += (offset.x + topLeftInFrameCoordinates.x);
+ phase.y += (offset.y + topLeftInFrameCoordinates.y);
+
+ // Align the phase to physical pixels so resizing the window under HiDPI
+ // doesn't cause wiggling of the theme.
+ phase = [frameView convertPointToBase:phase];
+ phase.x = floor(phase.x);
+ phase.y = floor(phase.y);
+ phase = [frameView convertPointFromBase:phase];
+
+ // Default to replacing any existing pixels with the theme image, but if
+ // asked paint black first and blend the theme with black.
+ NSCompositingOperation operation = NSCompositeCopy;
+ if (forceBlackBackground) {
+ [[NSColor blackColor] set];
+ NSRectFill(dirtyRect);
+ operation = NSCompositeSourceOver;
+ }
+
+ [[NSGraphicsContext currentContext] setPatternPhase:phase];
+ [themeImageColor set];
+ NSRectFillUsingOperation(dirtyRect, operation);
+ themed = YES;
+ } else if (gradient) {
+ NSPoint startPoint = NSMakePoint(NSMinX(bounds), NSMaxY(bounds));
+ NSPoint endPoint = startPoint;
+ endPoint.y -= kBrowserFrameViewPaintHeight;
+ [gradient drawFromPoint:startPoint toPoint:endPoint options:0];
+ themed = YES;
+ }
+
+ // Check to see if we have an overlay image.
+ NSImage* overlayImage = nil;
+ if (themeProvider->HasCustomImage(IDR_THEME_FRAME_OVERLAY)) {
+ overlayImage = themeProvider->
+ GetNSImageNamed(active ? IDR_THEME_FRAME_OVERLAY :
+ IDR_THEME_FRAME_OVERLAY_INACTIVE,
+ true);
+ }
+
+ if (overlayImage) {
+ // Anchor to top-left and don't scale.
+ NSSize overlaySize = [overlayImage size];
+ NSRect imageFrame = NSMakeRect(0, 0, overlaySize.width, overlaySize.height);
+ [overlayImage drawAtPoint:NSMakePoint(offset.x,
+ NSHeight(bounds) + offset.y -
+ overlaySize.height)
+ fromRect:imageFrame
+ operation:NSCompositeSourceOver
+ fraction:1.0];
+ }
+
+ return themed;
+}
+
++ (NSColor*)titleColorForThemeView:(NSView*)view {
+ ThemeProvider* themeProvider = [[view window] themeProvider];
+ if (!themeProvider)
+ return [NSColor windowFrameTextColor];
+
+ ThemedWindowStyle windowStyle = [[view window] themedWindowStyle];
+ BOOL active = [[view window] isMainWindow];
+ BOOL incognito = windowStyle & THEMED_INCOGNITO;
+ BOOL popup = windowStyle & THEMED_POPUP;
+
+ NSColor* titleColor = nil;
+ if (popup && active) {
+ titleColor = themeProvider->GetNSColor(
+ BrowserThemeProvider::COLOR_TAB_TEXT, false);
+ } else if (popup && !active) {
+ titleColor = themeProvider->GetNSColor(
+ BrowserThemeProvider::COLOR_BACKGROUND_TAB_TEXT, false);
+ }
+
+ if (titleColor)
+ return titleColor;
+
+ if (incognito)
+ return [NSColor whiteColor];
+ else
+ return [NSColor windowFrameTextColor];
+}
+
+// Check to see if the mouse is currently in one of our window widgets.
+- (BOOL)_mouseInGroup:(NSButton*)widget {
+ BOOL mouseInGroup = NO;
+ if ([[self window] isKindOfClass:[FramedBrowserWindow class]]) {
+ FramedBrowserWindow* window =
+ static_cast<FramedBrowserWindow*>([self window]);
+ mouseInGroup = [window mouseInGroup:widget];
+ } else if ([super respondsToSelector:@selector(_mouseInGroup:)]) {
+ mouseInGroup = [super _mouseInGroup:widget];
+ }
+ return mouseInGroup;
+}
+
+// Let our window handle updating the window widget tracking area.
+- (void)updateTrackingAreas {
+ [super updateTrackingAreas];
+ if ([[self window] isKindOfClass:[FramedBrowserWindow class]]) {
+ FramedBrowserWindow* window =
+ static_cast<FramedBrowserWindow*>([self window]);
+ [window updateTrackingAreas];
+ }
+}
+
+// When the compositor is active, the whole content area is transparent (with
+// an OpenGL surface behind it), so Cocoa draws the shadow only around the
+// toolbar area.
+// Tell the window server that we want a shadow as if none of the content
+// area is transparent.
+- (NSUInteger)_shadowFlags {
+ // A slightly less intrusive hack would be to call
+ // _setContentHasShadow:NO on the window. That seems to be what Terminal.app
+ // is doing. However, it leads to this function returning 'code | 64', which
+ // doesn't do what we want. For some reason, it does the right thing in
+ // Terminal.app.
+ // TODO(thakis): Figure out why -_setContentHasShadow: works in Terminal.app
+ // and use that technique instead. http://crbug.com/53382
+
+ // If this isn't the window class we expect, then pass it on to the
+ // original implementation.
+ if (![[self window] isKindOfClass:[FramedBrowserWindow class]])
+ return [self _shadowFlagsOriginal];
+
+ return [self _shadowFlagsOriginal] | 128;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/browser_frame_view_unittest.mm b/chrome/browser/ui/cocoa/browser_frame_view_unittest.mm
new file mode 100644
index 0000000..ca2de67
--- /dev/null
+++ b/chrome/browser/ui/cocoa/browser_frame_view_unittest.mm
@@ -0,0 +1,48 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+#include <objc/runtime.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/browser_frame_view.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+class BrowserFrameViewTest : public PlatformTest {
+ public:
+ BrowserFrameViewTest() {
+ NSRect frame = NSMakeRect(0, 0, 50, 50);
+ // We create NSGrayFrame instead of BrowserFrameView because
+ // we are swizzling into NSGrayFrame.
+ Class browserFrameClass = NSClassFromString(@"NSGrayFrame");
+ view_.reset([[browserFrameClass alloc] initWithFrame:frame]);
+ }
+
+ scoped_nsobject<NSView> view_;
+};
+
+// Test to make sure our class modifications were successful.
+TEST_F(BrowserFrameViewTest, SuccessfulClassModifications) {
+ unsigned int count;
+ BOOL foundMouseInGroup = NO;
+ BOOL foundDrawRectOriginal = NO;
+ BOOL foundUpdateTrackingAreas = NO;
+
+ Method* methods = class_copyMethodList([view_ class], &count);
+ for (unsigned int i = 0; i < count; ++i) {
+ SEL selector = method_getName(methods[i]);
+ if (selector == @selector(_mouseInGroup:)) {
+ foundMouseInGroup = YES;
+ } else if (selector == @selector(drawRectOriginal:)) {
+ foundDrawRectOriginal = YES;
+ } else if (selector == @selector(updateTrackingAreas)) {
+ foundUpdateTrackingAreas = YES;
+ }
+ }
+ EXPECT_TRUE(foundMouseInGroup);
+ EXPECT_TRUE(foundDrawRectOriginal);
+ EXPECT_TRUE(foundUpdateTrackingAreas);
+ free(methods);
+}
diff --git a/chrome/browser/ui/cocoa/browser_test_helper.h b/chrome/browser/ui/cocoa/browser_test_helper.h
new file mode 100644
index 0000000..4b5b6a9
--- /dev/null
+++ b/chrome/browser/ui/cocoa/browser_test_helper.h
@@ -0,0 +1,92 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_BROWSER_TEST_HELPER_H_
+#define CHROME_BROWSER_UI_COCOA_BROWSER_TEST_HELPER_H_
+#pragma once
+
+#include "chrome/browser/browser_thread.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/test/testing_profile.h"
+
+// Base class which contains a valid Browser*. Lots of boilerplate to
+// recycle between unit test classes.
+//
+// This class creates fake UI, file, and IO threads because many objects that
+// are attached to the TestingProfile (and other objects) have traits that limit
+// their destruction to certain threads. For example, the URLRequestContext can
+// only be deleted on the IO thread; without this fake IO thread, the object
+// would never be deleted and would report as a leak under Valgrind. Note that
+// these are fake threads and they all share the same MessageLoop.
+//
+// TODO(jrg): move up a level (chrome/browser/ui/cocoa -->
+// chrome/browser), and use in non-Mac unit tests such as
+// back_forward_menu_model_unittest.cc,
+// navigation_controller_unittest.cc, ..
+class BrowserTestHelper {
+ public:
+ BrowserTestHelper()
+ : ui_thread_(BrowserThread::UI, &message_loop_),
+ file_thread_(new BrowserThread(BrowserThread::FILE, &message_loop_)),
+ io_thread_(new BrowserThread(BrowserThread::IO, &message_loop_)) {
+ profile_.reset(new TestingProfile());
+ profile_->CreateBookmarkModel(true);
+ profile_->BlockUntilBookmarkModelLoaded();
+
+ // TODO(shess): These are needed in case someone creates a browser
+ // window off of browser_. pkasting indicates that other
+ // platforms use a stub |BrowserWindow| and thus don't need to do
+ // this.
+ // http://crbug.com/39725
+ profile_->CreateAutocompleteClassifier();
+ profile_->CreateTemplateURLModel();
+
+ browser_.reset(new Browser(Browser::TYPE_NORMAL, profile_.get()));
+ }
+
+ virtual ~BrowserTestHelper() {
+ // Delete the testing profile on the UI thread. But first release the
+ // browser, since it may trigger accesses to the profile upon destruction.
+ browser_.reset();
+
+ // Drop any new tasks for the IO and FILE threads.
+ io_thread_.reset();
+ file_thread_.reset();
+
+ message_loop_.DeleteSoon(FROM_HERE, profile_.release());
+ message_loop_.RunAllPending();
+ }
+
+ virtual TestingProfile* profile() const { return profile_.get(); }
+ Browser* browser() const { return browser_.get(); }
+
+ // Creates the browser window. To close this window call |CloseBrowserWindow|.
+ // Do NOT call close directly on the window.
+ BrowserWindow* CreateBrowserWindow() {
+ browser_->CreateBrowserWindow();
+ return browser_->window();
+ }
+
+ // Closes the window for this browser. This must only be called after
+ // CreateBrowserWindow().
+ void CloseBrowserWindow() {
+ // Check to make sure a window was actually created.
+ DCHECK(browser_->window());
+ browser_->CloseAllTabs();
+ browser_->CloseWindow();
+ // |browser_| will be deleted by its BrowserWindowController.
+ ignore_result(browser_.release());
+ }
+
+ private:
+ scoped_ptr<TestingProfile> profile_;
+ scoped_ptr<Browser> browser_;
+ MessageLoopForUI message_loop_;
+ BrowserThread ui_thread_;
+ scoped_ptr<BrowserThread> file_thread_;
+ scoped_ptr<BrowserThread> io_thread_;
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_BROWSER_TEST_HELPER_H_
diff --git a/chrome/browser/ui/cocoa/browser_window_cocoa.h b/chrome/browser/ui/cocoa/browser_window_cocoa.h
new file mode 100644
index 0000000..316d062
--- /dev/null
+++ b/chrome/browser/ui/cocoa/browser_window_cocoa.h
@@ -0,0 +1,143 @@
+// 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_UI_COCOA_BROWSER_WINDOW_COCOA_H_
+#define CHROME_BROWSER_UI_COCOA_BROWSER_WINDOW_COCOA_H_
+#pragma once
+
+#include "base/scoped_nsobject.h"
+#include "base/task.h"
+#include "chrome/browser/browser_window.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/common/notification_registrar.h"
+
+class Browser;
+@class BrowserWindowController;
+@class FindBarCocoaController;
+@class NSEvent;
+@class NSMenu;
+@class NSWindow;
+
+// An implementation of BrowserWindow for Cocoa. Bridges between C++ and
+// the Cocoa NSWindow. Cross-platform code will interact with this object when
+// it needs to manipulate the window.
+
+class BrowserWindowCocoa : public BrowserWindow,
+ public NotificationObserver {
+ public:
+ BrowserWindowCocoa(Browser* browser,
+ BrowserWindowController* controller,
+ NSWindow* window);
+ virtual ~BrowserWindowCocoa();
+
+ // Overridden from BrowserWindow
+ virtual void Show();
+ virtual void SetBounds(const gfx::Rect& bounds);
+ virtual void Close();
+ virtual void Activate();
+ virtual void Deactivate();
+ virtual bool IsActive() const;
+ virtual void FlashFrame();
+ virtual gfx::NativeWindow GetNativeHandle();
+ virtual BrowserWindowTesting* GetBrowserWindowTesting();
+ virtual StatusBubble* GetStatusBubble();
+ virtual void SelectedTabToolbarSizeChanged(bool is_animating);
+ virtual void UpdateTitleBar();
+ virtual void ShelfVisibilityChanged();
+ virtual void UpdateDevTools();
+ virtual void UpdateLoadingAnimations(bool should_animate);
+ virtual void SetStarredState(bool is_starred);
+ virtual gfx::Rect GetRestoredBounds() const;
+ virtual bool IsMaximized() const;
+ virtual void SetFullscreen(bool fullscreen);
+ virtual bool IsFullscreen() const;
+ virtual bool IsFullscreenBubbleVisible() const;
+ virtual LocationBar* GetLocationBar() const;
+ virtual void SetFocusToLocationBar(bool select_all);
+ virtual void UpdateReloadStopState(bool is_loading, bool force);
+ virtual void UpdateToolbar(TabContentsWrapper* contents,
+ bool should_restore_state);
+ virtual void FocusToolbar();
+ virtual void FocusAppMenu();
+ virtual void FocusBookmarksToolbar();
+ virtual void FocusChromeOSStatus();
+ virtual void RotatePaneFocus(bool forwards);
+ virtual bool IsBookmarkBarVisible() const;
+ virtual bool IsBookmarkBarAnimating() const;
+ virtual bool IsToolbarVisible() const;
+ virtual void ConfirmAddSearchProvider(const TemplateURL* template_url,
+ Profile* profile);
+ virtual void ToggleBookmarkBar();
+ virtual views::Window* ShowAboutChromeDialog();
+ virtual void ShowUpdateChromeDialog();
+ virtual void ShowTaskManager();
+ virtual void ShowBookmarkBubble(const GURL& url, bool already_bookmarked);
+ virtual bool IsDownloadShelfVisible() const;
+ virtual DownloadShelf* GetDownloadShelf();
+ virtual void ShowReportBugDialog();
+ virtual void ShowClearBrowsingDataDialog();
+ virtual void ShowImportDialog();
+ virtual void ShowSearchEnginesDialog();
+ virtual void ShowPasswordManager();
+ virtual void ShowRepostFormWarningDialog(TabContents* tab_contents);
+ virtual void ShowContentSettingsWindow(ContentSettingsType content_type,
+ Profile* profile);
+ virtual void ShowCollectedCookiesDialog(TabContents* tab_contents);
+ virtual void ShowProfileErrorDialog(int message_id);
+ virtual void ShowThemeInstallBubble();
+ virtual void ConfirmBrowserCloseWithPendingDownloads();
+ virtual void ShowHTMLDialog(HtmlDialogUIDelegate* delegate,
+ gfx::NativeWindow parent_window);
+ virtual void UserChangedTheme();
+ virtual int GetExtraRenderViewHeight() const;
+ virtual void TabContentsFocused(TabContents* tab_contents);
+ virtual void ShowPageInfo(Profile* profile,
+ const GURL& url,
+ const NavigationEntry::SSLStatus& ssl,
+ bool show_history);
+ virtual void ShowAppMenu();
+ virtual bool PreHandleKeyboardEvent(const NativeWebKeyboardEvent& event,
+ bool* is_keyboard_shortcut);
+ virtual void HandleKeyboardEvent(const NativeWebKeyboardEvent& event);
+ virtual void ShowCreateWebAppShortcutsDialog(TabContents* tab_contents);
+ virtual void ShowCreateChromeAppShortcutsDialog(Profile* profile,
+ const Extension* app);
+ virtual void Cut();
+ virtual void Copy();
+ virtual void Paste();
+ virtual void ToggleTabStripMode();
+ virtual void OpenTabpose();
+ virtual void PrepareForInstant();
+ virtual void ShowInstant(TabContents* preview_contents);
+ virtual void HideInstant();
+ virtual gfx::Rect GetInstantBounds();
+
+ // Overridden from NotificationObserver
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+
+ // Adds the given FindBar cocoa controller to this browser window.
+ void AddFindBar(FindBarCocoaController* find_bar_cocoa_controller);
+
+ // Returns the cocoa-world BrowserWindowController
+ BrowserWindowController* cocoa_controller() { return controller_; }
+
+ protected:
+ virtual void DestroyBrowser();
+
+ private:
+ int GetCommandId(const NativeWebKeyboardEvent& event);
+ bool HandleKeyboardEventInternal(NSEvent* event);
+ NSWindow* window() const; // Accessor for the (current) |NSWindow|.
+ void UpdateSidebarForContents(TabContents* tab_contents);
+
+ NotificationRegistrar registrar_;
+ Browser* browser_; // weak, owned by controller
+ BrowserWindowController* controller_; // weak, owns us
+ ScopedRunnableMethodFactory<Browser> confirm_close_factory_;
+ scoped_nsobject<NSString> pending_window_title_;
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_BROWSER_WINDOW_COCOA_H_
diff --git a/chrome/browser/ui/cocoa/browser_window_cocoa.mm b/chrome/browser/ui/cocoa/browser_window_cocoa.mm
new file mode 100644
index 0000000..003fec7
--- /dev/null
+++ b/chrome/browser/ui/cocoa/browser_window_cocoa.mm
@@ -0,0 +1,638 @@
+// 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 "chrome/browser/ui/cocoa/browser_window_cocoa.h"
+
+#include "app/l10n_util_mac.h"
+#include "base/command_line.h"
+#include "base/logging.h"
+#include "base/message_loop.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/bookmarks/bookmark_utils.h"
+#include "chrome/browser/download/download_shelf.h"
+#include "chrome/browser/global_keyboard_shortcuts_mac.h"
+#include "chrome/browser/page_info_window.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/sidebar/sidebar_container.h"
+#include "chrome/browser/sidebar/sidebar_manager.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents_wrapper.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/bug_report_window_controller.h"
+#import "chrome/browser/ui/cocoa/chrome_event_processing_window.h"
+#import "chrome/browser/ui/cocoa/clear_browsing_data_controller.h"
+#import "chrome/browser/ui/cocoa/collected_cookies_mac.h"
+#import "chrome/browser/ui/cocoa/content_settings_dialog_controller.h"
+#import "chrome/browser/ui/cocoa/download/download_shelf_controller.h"
+#import "chrome/browser/ui/cocoa/edit_search_engine_cocoa_controller.h"
+#import "chrome/browser/ui/cocoa/html_dialog_window_controller.h"
+#import "chrome/browser/ui/cocoa/import_settings_dialog.h"
+#import "chrome/browser/ui/cocoa/keyword_editor_cocoa_controller.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
+#import "chrome/browser/ui/cocoa/nsmenuitem_additions.h"
+#include "chrome/browser/ui/cocoa/repost_form_warning_mac.h"
+#include "chrome/browser/ui/cocoa/restart_browser.h"
+#include "chrome/browser/ui/cocoa/status_bubble_mac.h"
+#include "chrome/browser/ui/cocoa/task_manager_mac.h"
+#import "chrome/browser/ui/cocoa/theme_install_bubble_view.h"
+#import "chrome/browser/ui/cocoa/toolbar_controller.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/common/native_web_keyboard_event.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/pref_names.h"
+#include "gfx/rect.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+
+BrowserWindowCocoa::BrowserWindowCocoa(Browser* browser,
+ BrowserWindowController* controller,
+ NSWindow* window)
+ : browser_(browser),
+ controller_(controller),
+ confirm_close_factory_(browser) {
+ // This pref applies to all windows, so all must watch for it.
+ registrar_.Add(this, NotificationType::BOOKMARK_BAR_VISIBILITY_PREF_CHANGED,
+ NotificationService::AllSources());
+ registrar_.Add(this, NotificationType::SIDEBAR_CHANGED,
+ NotificationService::AllSources());
+}
+
+BrowserWindowCocoa::~BrowserWindowCocoa() {
+}
+
+void BrowserWindowCocoa::Show() {
+ // The Browser associated with this browser window must become the active
+ // browser at the time |Show()| is called. This is the natural behaviour under
+ // Windows, but |-makeKeyAndOrderFront:| won't send |-windowDidBecomeMain:|
+ // until we return to the runloop. Therefore any calls to
+ // |BrowserList::GetLastActive()| (for example, in bookmark_util), will return
+ // the previous browser instead if we don't explicitly set it here.
+ BrowserList::SetLastActive(browser_);
+
+ [window() makeKeyAndOrderFront:controller_];
+}
+
+void BrowserWindowCocoa::SetBounds(const gfx::Rect& bounds) {
+ NSRect cocoa_bounds = NSMakeRect(bounds.x(), 0, bounds.width(),
+ bounds.height());
+ // Flip coordinates based on the primary screen.
+ NSScreen* screen = [[NSScreen screens] objectAtIndex:0];
+ cocoa_bounds.origin.y =
+ [screen frame].size.height - bounds.height() - bounds.y();
+
+ [window() setFrame:cocoa_bounds display:YES];
+}
+
+// Callers assume that this doesn't immediately delete the Browser object.
+// The controller implementing the window delegate methods called from
+// |-performClose:| must take precautions to ensure that.
+void BrowserWindowCocoa::Close() {
+ // If there is an overlay window, we contain a tab being dragged between
+ // windows. Don't hide the window as it makes the UI extra confused. We can
+ // still close the window, as that will happen when the drag completes.
+ if ([controller_ overlayWindow]) {
+ [controller_ deferPerformClose];
+ } else {
+ // Make sure we hide the window immediately. Even though performClose:
+ // calls orderOut: eventually, it leaves the window on-screen long enough
+ // that we start to see tabs shutting down. http://crbug.com/23959
+ // TODO(viettrungluu): This is kind of bad, since |-performClose:| calls
+ // |-windowShouldClose:| (on its delegate, which is probably the
+ // controller) which may return |NO| causing the window to not be closed,
+ // thereby leaving a hidden window. In fact, our window-closing procedure
+ // involves a (indirect) recursion on |-performClose:|, which is also bad.
+ [window() orderOut:controller_];
+ [window() performClose:controller_];
+ }
+}
+
+void BrowserWindowCocoa::Activate() {
+ [controller_ activate];
+}
+
+void BrowserWindowCocoa::Deactivate() {
+ // TODO(jcivelli): http://crbug.com/51364 Implement me.
+ NOTIMPLEMENTED();
+}
+
+void BrowserWindowCocoa::FlashFrame() {
+ [NSApp requestUserAttention:NSInformationalRequest];
+}
+
+bool BrowserWindowCocoa::IsActive() const {
+ return [window() isKeyWindow];
+}
+
+gfx::NativeWindow BrowserWindowCocoa::GetNativeHandle() {
+ return window();
+}
+
+BrowserWindowTesting* BrowserWindowCocoa::GetBrowserWindowTesting() {
+ return NULL;
+}
+
+StatusBubble* BrowserWindowCocoa::GetStatusBubble() {
+ return [controller_ statusBubble];
+}
+
+void BrowserWindowCocoa::SelectedTabToolbarSizeChanged(bool is_animating) {
+ // According to beng, this is an ugly method that comes from the days when the
+ // download shelf was a ChromeView attached to the TabContents, and as its
+ // size changed via animation it notified through TCD/etc to the browser view
+ // to relayout for each tick of the animation. We don't need anything of the
+ // sort on Mac.
+}
+
+void BrowserWindowCocoa::UpdateTitleBar() {
+ NSString* newTitle =
+ base::SysUTF16ToNSString(browser_->GetWindowTitleForCurrentTab());
+
+ // Work around Cocoa bug: if a window changes title during the tracking of the
+ // Window menu it doesn't display well and the constant re-sorting of the list
+ // makes it difficult for the user to pick the desired window. Delay window
+ // title updates until the default run-loop mode.
+
+ if (pending_window_title_.get())
+ [[NSRunLoop currentRunLoop]
+ cancelPerformSelector:@selector(setTitle:)
+ target:window()
+ argument:pending_window_title_.get()];
+
+ pending_window_title_.reset([newTitle copy]);
+ [[NSRunLoop currentRunLoop]
+ performSelector:@selector(setTitle:)
+ target:window()
+ argument:newTitle
+ order:0
+ modes:[NSArray arrayWithObject:NSDefaultRunLoopMode]];
+}
+
+void BrowserWindowCocoa::ShelfVisibilityChanged() {
+ // Mac doesn't yet support showing the bookmark bar at a different size on
+ // the new tab page. When it does, this method should attempt to relayout the
+ // bookmark bar/extension shelf as their preferred height may have changed.
+ // http://crbug.com/43346
+}
+
+void BrowserWindowCocoa::UpdateDevTools() {
+ [controller_ updateDevToolsForContents:
+ browser_->GetSelectedTabContents()];
+}
+
+void BrowserWindowCocoa::UpdateLoadingAnimations(bool should_animate) {
+ // Do nothing on Mac.
+}
+
+void BrowserWindowCocoa::SetStarredState(bool is_starred) {
+ [controller_ setStarredState:is_starred ? YES : NO];
+}
+
+gfx::Rect BrowserWindowCocoa::GetRestoredBounds() const {
+ // Flip coordinates based on the primary screen.
+ NSScreen* screen = [[NSScreen screens] objectAtIndex:0];
+ NSRect frame = [controller_ regularWindowFrame];
+ gfx::Rect bounds(frame.origin.x, 0, frame.size.width, frame.size.height);
+ bounds.set_y([screen frame].size.height - frame.origin.y - frame.size.height);
+ return bounds;
+}
+
+bool BrowserWindowCocoa::IsMaximized() const {
+ return [window() isZoomed];
+}
+
+void BrowserWindowCocoa::SetFullscreen(bool fullscreen) {
+ [controller_ setFullscreen:fullscreen];
+}
+
+bool BrowserWindowCocoa::IsFullscreen() const {
+ return !![controller_ isFullscreen];
+}
+
+bool BrowserWindowCocoa::IsFullscreenBubbleVisible() const {
+ return false;
+}
+
+void BrowserWindowCocoa::ConfirmAddSearchProvider(
+ const TemplateURL* template_url,
+ Profile* profile) {
+ // The controller will release itself when the window closes.
+ EditSearchEngineCocoaController* editor =
+ [[EditSearchEngineCocoaController alloc] initWithProfile:profile
+ delegate:NULL
+ templateURL:template_url];
+ [NSApp beginSheet:[editor window]
+ modalForWindow:window()
+ modalDelegate:controller_
+ didEndSelector:@selector(sheetDidEnd:returnCode:context:)
+ contextInfo:NULL];
+}
+
+LocationBar* BrowserWindowCocoa::GetLocationBar() const {
+ return [controller_ locationBarBridge];
+}
+
+void BrowserWindowCocoa::SetFocusToLocationBar(bool select_all) {
+ [controller_ focusLocationBar:select_all ? YES : NO];
+}
+
+void BrowserWindowCocoa::UpdateReloadStopState(bool is_loading, bool force) {
+ [controller_ setIsLoading:is_loading force:force];
+}
+
+void BrowserWindowCocoa::UpdateToolbar(TabContentsWrapper* contents,
+ bool should_restore_state) {
+ [controller_ updateToolbarWithContents:contents->tab_contents()
+ shouldRestoreState:should_restore_state ? YES : NO];
+}
+
+void BrowserWindowCocoa::FocusToolbar() {
+ // Not needed on the Mac.
+}
+
+void BrowserWindowCocoa::FocusAppMenu() {
+ // Chrome uses the standard Mac OS X menu bar, so this isn't needed.
+}
+
+void BrowserWindowCocoa::RotatePaneFocus(bool forwards) {
+ // Not needed on the Mac.
+}
+
+void BrowserWindowCocoa::FocusBookmarksToolbar() {
+ // Not needed on the Mac.
+}
+
+void BrowserWindowCocoa::FocusChromeOSStatus() {
+ // Not needed on the Mac.
+}
+
+bool BrowserWindowCocoa::IsBookmarkBarVisible() const {
+ return browser_->profile()->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar);
+}
+
+bool BrowserWindowCocoa::IsBookmarkBarAnimating() const {
+ return [controller_ isBookmarkBarAnimating];
+}
+
+bool BrowserWindowCocoa::IsToolbarVisible() const {
+ return browser_->SupportsWindowFeature(Browser::FEATURE_TOOLBAR) ||
+ browser_->SupportsWindowFeature(Browser::FEATURE_LOCATIONBAR);
+}
+
+// This is called from Browser, which in turn is called directly from
+// a menu option. All we do here is set a preference. The act of
+// setting the preference sends notifications to all windows who then
+// know what to do.
+void BrowserWindowCocoa::ToggleBookmarkBar() {
+ bookmark_utils::ToggleWhenVisible(browser_->profile());
+}
+
+void BrowserWindowCocoa::AddFindBar(
+ FindBarCocoaController* find_bar_cocoa_controller) {
+ return [controller_ addFindBar:find_bar_cocoa_controller];
+}
+
+views::Window* BrowserWindowCocoa::ShowAboutChromeDialog() {
+ NOTIMPLEMENTED();
+ return NULL;
+}
+
+void BrowserWindowCocoa::ShowUpdateChromeDialog() {
+ restart_browser::RequestRestart(nil);
+}
+
+void BrowserWindowCocoa::ShowTaskManager() {
+ TaskManagerMac::Show();
+}
+
+void BrowserWindowCocoa::ShowBookmarkBubble(const GURL& url,
+ bool already_bookmarked) {
+ [controller_ showBookmarkBubbleForURL:url
+ alreadyBookmarked:(already_bookmarked ? YES : NO)];
+}
+
+bool BrowserWindowCocoa::IsDownloadShelfVisible() const {
+ return [controller_ isDownloadShelfVisible] != NO;
+}
+
+DownloadShelf* BrowserWindowCocoa::GetDownloadShelf() {
+ DownloadShelfController* shelfController = [controller_ downloadShelf];
+ return [shelfController bridge];
+}
+
+void BrowserWindowCocoa::ShowReportBugDialog() {
+ TabContents* current_tab = browser_->GetSelectedTabContents();
+ if (current_tab && current_tab->controller().GetActiveEntry()) {
+ browser_->ShowBrokenPageTab(current_tab);
+ }
+}
+
+void BrowserWindowCocoa::ShowClearBrowsingDataDialog() {
+ [ClearBrowsingDataController
+ showClearBrowsingDialogForProfile:browser_->profile()];
+}
+
+void BrowserWindowCocoa::ShowImportDialog() {
+ [ImportSettingsDialogController
+ showImportSettingsDialogForProfile:browser_->profile()];
+}
+
+void BrowserWindowCocoa::ShowSearchEnginesDialog() {
+ [KeywordEditorCocoaController showKeywordEditor:browser_->profile()];
+}
+
+void BrowserWindowCocoa::ShowPasswordManager() {
+ NOTIMPLEMENTED();
+}
+
+void BrowserWindowCocoa::ShowRepostFormWarningDialog(
+ TabContents* tab_contents) {
+ RepostFormWarningMac::Create(GetNativeHandle(), tab_contents);
+}
+
+void BrowserWindowCocoa::ShowContentSettingsWindow(
+ ContentSettingsType settings_type,
+ Profile* profile) {
+ [ContentSettingsDialogController showContentSettingsForType:settings_type
+ profile:profile];
+}
+
+void BrowserWindowCocoa::ShowCollectedCookiesDialog(TabContents* tab_contents) {
+ // Deletes itself on close.
+ new CollectedCookiesMac(GetNativeHandle(), tab_contents);
+}
+
+void BrowserWindowCocoa::ShowProfileErrorDialog(int message_id) {
+ scoped_nsobject<NSAlert> alert([[NSAlert alloc] init]);
+ [alert addButtonWithTitle:l10n_util::GetNSStringWithFixup(IDS_OK)];
+ [alert setMessageText:l10n_util::GetNSStringWithFixup(IDS_PRODUCT_NAME)];
+ [alert setInformativeText:l10n_util::GetNSStringWithFixup(message_id)];
+ [alert setAlertStyle:NSWarningAlertStyle];
+ [alert runModal];
+}
+
+void BrowserWindowCocoa::ShowThemeInstallBubble() {
+ ThemeInstallBubbleView::Show(window());
+}
+
+// We allow closing the window here since the real quit decision on Mac is made
+// in [AppController quit:].
+void BrowserWindowCocoa::ConfirmBrowserCloseWithPendingDownloads() {
+ // Call InProgressDownloadResponse asynchronously to avoid a crash when the
+ // browser window is closed here (http://crbug.com/44454).
+ MessageLoop::current()->PostTask(
+ FROM_HERE,
+ confirm_close_factory_.NewRunnableMethod(
+ &Browser::InProgressDownloadResponse,
+ true));
+}
+
+void BrowserWindowCocoa::ShowHTMLDialog(HtmlDialogUIDelegate* delegate,
+ gfx::NativeWindow parent_window) {
+ [HtmlDialogWindowController showHtmlDialog:delegate
+ profile:browser_->profile()];
+}
+
+void BrowserWindowCocoa::UserChangedTheme() {
+ [controller_ userChangedTheme];
+}
+
+int BrowserWindowCocoa::GetExtraRenderViewHeight() const {
+ // Currently this is only used on linux.
+ return 0;
+}
+
+void BrowserWindowCocoa::TabContentsFocused(TabContents* tab_contents) {
+ NOTIMPLEMENTED();
+}
+
+void BrowserWindowCocoa::ShowPageInfo(Profile* profile,
+ const GURL& url,
+ const NavigationEntry::SSLStatus& ssl,
+ bool show_history) {
+ browser::ShowPageInfoBubble(window(), profile, url, ssl, show_history);
+}
+
+void BrowserWindowCocoa::ShowAppMenu() {
+ // No-op. Mac doesn't support showing the menus via alt keys.
+}
+
+bool BrowserWindowCocoa::PreHandleKeyboardEvent(
+ const NativeWebKeyboardEvent& event, bool* is_keyboard_shortcut) {
+ if (event.skip_in_browser || event.type == NativeWebKeyboardEvent::Char)
+ return false;
+
+ DCHECK(event.os_event != NULL);
+ int id = GetCommandId(event);
+ if (id == -1)
+ return false;
+
+ if (browser_->IsReservedCommand(id))
+ return HandleKeyboardEventInternal(event.os_event);
+
+ DCHECK(is_keyboard_shortcut != NULL);
+ *is_keyboard_shortcut = true;
+
+ return false;
+}
+
+void BrowserWindowCocoa::HandleKeyboardEvent(
+ const NativeWebKeyboardEvent& event) {
+ if (event.skip_in_browser || event.type == NativeWebKeyboardEvent::Char)
+ return;
+
+ DCHECK(event.os_event != NULL);
+ HandleKeyboardEventInternal(event.os_event);
+}
+
+@interface MenuWalker : NSObject
++ (NSMenuItem*)itemForKeyEquivalent:(NSEvent*)key
+ menu:(NSMenu*)menu;
+@end
+
+@implementation MenuWalker
++ (NSMenuItem*)itemForKeyEquivalent:(NSEvent*)key
+ menu:(NSMenu*)menu {
+ NSMenuItem* result = nil;
+
+ for (NSMenuItem *item in [menu itemArray]) {
+ NSMenu* submenu = [item submenu];
+ if (submenu) {
+ if (submenu != [NSApp servicesMenu])
+ result = [self itemForKeyEquivalent:key
+ menu:submenu];
+ } else if ([item cr_firesForKeyEvent:key]) {
+ result = item;
+ }
+
+ if (result)
+ break;
+ }
+
+ return result;
+}
+@end
+
+int BrowserWindowCocoa::GetCommandId(const NativeWebKeyboardEvent& event) {
+ if ([event.os_event type] != NSKeyDown)
+ return -1;
+
+ // Look in menu.
+ NSMenuItem* item = [MenuWalker itemForKeyEquivalent:event.os_event
+ menu:[NSApp mainMenu]];
+
+ if (item && [item action] == @selector(commandDispatch:) && [item tag] > 0)
+ return [item tag];
+
+ // "Close window" doesn't use the |commandDispatch:| mechanism. Menu items
+ // that do not correspond to IDC_ constants need no special treatment however,
+ // as they can't be blacklisted in |Browser::IsReservedCommand()| anyhow.
+ if (item && [item action] == @selector(performClose:))
+ return IDC_CLOSE_WINDOW;
+
+ // "Exit" doesn't use the |commandDispatch:| mechanism either.
+ if (item && [item action] == @selector(terminate:))
+ return IDC_EXIT;
+
+ // Look in secondary keyboard shortcuts.
+ NSUInteger modifiers = [event.os_event modifierFlags];
+ const bool cmdKey = (modifiers & NSCommandKeyMask) != 0;
+ const bool shiftKey = (modifiers & NSShiftKeyMask) != 0;
+ const bool cntrlKey = (modifiers & NSControlKeyMask) != 0;
+ const bool optKey = (modifiers & NSAlternateKeyMask) != 0;
+ const int keyCode = [event.os_event keyCode];
+ const unichar keyChar = KeyCharacterForEvent(event.os_event);
+
+ int cmdNum = CommandForWindowKeyboardShortcut(
+ cmdKey, shiftKey, cntrlKey, optKey, keyCode, keyChar);
+ if (cmdNum != -1)
+ return cmdNum;
+
+ cmdNum = CommandForBrowserKeyboardShortcut(
+ cmdKey, shiftKey, cntrlKey, optKey, keyCode, keyChar);
+ if (cmdNum != -1)
+ return cmdNum;
+
+ return -1;
+}
+
+bool BrowserWindowCocoa::HandleKeyboardEventInternal(NSEvent* event) {
+ ChromeEventProcessingWindow* event_window =
+ static_cast<ChromeEventProcessingWindow*>(window());
+ DCHECK([event_window isKindOfClass:[ChromeEventProcessingWindow class]]);
+
+ // Do not fire shortcuts on key up.
+ if ([event type] == NSKeyDown) {
+ // Send the event to the menu before sending it to the browser/window
+ // shortcut handling, so that if a user configures cmd-left to mean
+ // "previous tab", it takes precedence over the built-in "history back"
+ // binding. Other than that, the |-redispatchKeyEvent:| call would take care
+ // of invoking the original menu item shortcut as well.
+
+ if ([[NSApp mainMenu] performKeyEquivalent:event])
+ return true;
+
+ if ([event_window handleExtraBrowserKeyboardShortcut:event])
+ return true;
+
+ if ([event_window handleExtraWindowKeyboardShortcut:event])
+ return true;
+
+ if ([event_window handleDelayedWindowKeyboardShortcut:event])
+ return true;
+ }
+
+ return [event_window redispatchKeyEvent:event];
+}
+
+void BrowserWindowCocoa::ShowCreateWebAppShortcutsDialog(
+ TabContents* tab_contents) {
+ NOTIMPLEMENTED();
+}
+
+void BrowserWindowCocoa::ShowCreateChromeAppShortcutsDialog(
+ Profile* profile, const Extension* app) {
+ NOTIMPLEMENTED();
+}
+
+void BrowserWindowCocoa::Cut() {
+ [NSApp sendAction:@selector(cut:) to:nil from:nil];
+}
+
+void BrowserWindowCocoa::Copy() {
+ [NSApp sendAction:@selector(copy:) to:nil from:nil];
+}
+
+void BrowserWindowCocoa::Paste() {
+ [NSApp sendAction:@selector(paste:) to:nil from:nil];
+}
+
+void BrowserWindowCocoa::ToggleTabStripMode() {
+ [controller_ toggleTabStripDisplayMode];
+}
+
+void BrowserWindowCocoa::OpenTabpose() {
+ [controller_ openTabpose];
+}
+
+void BrowserWindowCocoa::PrepareForInstant() {
+ // TODO: implement fade as done on windows.
+}
+
+void BrowserWindowCocoa::ShowInstant(TabContents* preview_contents) {
+ [controller_ showInstant:preview_contents];
+}
+
+void BrowserWindowCocoa::HideInstant() {
+ [controller_ hideInstant];
+}
+
+gfx::Rect BrowserWindowCocoa::GetInstantBounds() {
+ // Flip coordinates based on the primary screen.
+ NSScreen* screen = [[NSScreen screens] objectAtIndex:0];
+ NSRect monitorFrame = [screen frame];
+ NSRect frame = [controller_ instantFrame];
+ gfx::Rect bounds(NSRectToCGRect(frame));
+ bounds.set_y(NSHeight(monitorFrame) - bounds.y() - bounds.height());
+ return bounds;
+}
+
+void BrowserWindowCocoa::Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ switch (type.value) {
+ // Only the key window gets a direct toggle from the menu.
+ // Other windows hear about it from the notification.
+ case NotificationType::BOOKMARK_BAR_VISIBILITY_PREF_CHANGED:
+ [controller_ updateBookmarkBarVisibilityWithAnimation:YES];
+ break;
+ case NotificationType::SIDEBAR_CHANGED:
+ UpdateSidebarForContents(
+ Details<SidebarContainer>(details)->tab_contents());
+ break;
+ default:
+ NOTREACHED(); // we don't ask for anything else!
+ break;
+ }
+}
+
+void BrowserWindowCocoa::DestroyBrowser() {
+ [controller_ destroyBrowser];
+
+ // at this point the controller is dead (autoreleased), so
+ // make sure we don't try to reference it any more.
+}
+
+NSWindow* BrowserWindowCocoa::window() const {
+ return [controller_ window];
+}
+
+void BrowserWindowCocoa::UpdateSidebarForContents(TabContents* tab_contents) {
+ if (tab_contents == browser_->GetSelectedTabContents()) {
+ [controller_ updateSidebarForContents:tab_contents];
+ }
+}
diff --git a/chrome/browser/ui/cocoa/browser_window_cocoa_unittest.mm b/chrome/browser/ui/cocoa/browser_window_cocoa_unittest.mm
new file mode 100644
index 0000000..c76d3ee
--- /dev/null
+++ b/chrome/browser/ui/cocoa/browser_window_cocoa_unittest.mm
@@ -0,0 +1,120 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "base/string_util.h"
+#include "chrome/browser/bookmarks/bookmark_utils.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/browser_window_cocoa.h"
+#include "chrome/browser/ui/cocoa/browser_window_controller.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/common/notification_type.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+// A BrowserWindowCocoa that goes PONG when
+// BOOKMARK_BAR_VISIBILITY_PREF_CHANGED is sent. This is so we can be
+// sure we are observing it.
+class BrowserWindowCocoaPong : public BrowserWindowCocoa {
+ public:
+ BrowserWindowCocoaPong(Browser* browser,
+ BrowserWindowController* controller) :
+ BrowserWindowCocoa(browser, controller, [controller window]) {
+ pong_ = false;
+ }
+ virtual ~BrowserWindowCocoaPong() { }
+
+ void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ if (type.value == NotificationType::BOOKMARK_BAR_VISIBILITY_PREF_CHANGED)
+ pong_ = true;
+ BrowserWindowCocoa::Observe(type, source, details);
+ }
+
+ bool pong_;
+};
+
+// Main test class.
+class BrowserWindowCocoaTest : public CocoaTest {
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ Browser* browser = browser_helper_.browser();
+ controller_ = [[BrowserWindowController alloc] initWithBrowser:browser
+ takeOwnership:NO];
+ }
+
+ virtual void TearDown() {
+ [controller_ close];
+ CocoaTest::TearDown();
+ }
+
+ public:
+ BrowserTestHelper browser_helper_;
+ BrowserWindowController* controller_;
+};
+
+
+TEST_F(BrowserWindowCocoaTest, TestNotification) {
+ BrowserWindowCocoaPong *bwc =
+ new BrowserWindowCocoaPong(browser_helper_.browser(), controller_);
+
+ EXPECT_FALSE(bwc->pong_);
+ bookmark_utils::ToggleWhenVisible(browser_helper_.profile());
+ // Confirm we are listening
+ EXPECT_TRUE(bwc->pong_);
+ delete bwc;
+ // If this does NOT crash it confirms we stopped listening in the destructor.
+ bookmark_utils::ToggleWhenVisible(browser_helper_.profile());
+}
+
+
+TEST_F(BrowserWindowCocoaTest, TestBookmarkBarVisible) {
+ BrowserWindowCocoaPong *bwc = new BrowserWindowCocoaPong(
+ browser_helper_.browser(),
+ controller_);
+ scoped_ptr<BrowserWindowCocoaPong> scoped_bwc(bwc);
+
+ bool before = bwc->IsBookmarkBarVisible();
+ bookmark_utils::ToggleWhenVisible(browser_helper_.profile());
+ EXPECT_NE(before, bwc->IsBookmarkBarVisible());
+
+ bookmark_utils::ToggleWhenVisible(browser_helper_.profile());
+ EXPECT_EQ(before, bwc->IsBookmarkBarVisible());
+}
+
+@interface FakeController : NSWindowController {
+ BOOL fullscreen_;
+}
+@end
+
+@implementation FakeController
+- (void)setFullscreen:(BOOL)fullscreen {
+ fullscreen_ = fullscreen;
+}
+- (BOOL)isFullscreen {
+ return fullscreen_;
+}
+@end
+
+TEST_F(BrowserWindowCocoaTest, TestFullscreen) {
+ // Wrap the FakeController in a scoped_nsobject instead of autoreleasing in
+ // windowWillClose: because we never actually open a window in this test (so
+ // windowWillClose: never gets called).
+ scoped_nsobject<FakeController> fake_controller(
+ [[FakeController alloc] init]);
+ BrowserWindowCocoaPong *bwc = new BrowserWindowCocoaPong(
+ browser_helper_.browser(),
+ (BrowserWindowController*)fake_controller.get());
+ scoped_ptr<BrowserWindowCocoaPong> scoped_bwc(bwc);
+
+ EXPECT_FALSE(bwc->IsFullscreen());
+ bwc->SetFullscreen(true);
+ EXPECT_TRUE(bwc->IsFullscreen());
+ bwc->SetFullscreen(false);
+ EXPECT_FALSE(bwc->IsFullscreen());
+ [fake_controller close];
+}
+
+// TODO(???): test other methods of BrowserWindowCocoa
diff --git a/chrome/browser/ui/cocoa/browser_window_controller.h b/chrome/browser/ui/cocoa/browser_window_controller.h
new file mode 100644
index 0000000..4ff053e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/browser_window_controller.h
@@ -0,0 +1,397 @@
+// 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_UI_COCOA_BROWSER_WINDOW_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_BROWSER_WINDOW_CONTROLLER_H_
+#pragma once
+
+// A class acting as the Objective-C controller for the Browser
+// object. Handles interactions between Cocoa and the cross-platform
+// code. Each window has a single toolbar and, by virtue of being a
+// TabWindowController, a tab strip along the top.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/sync/sync_ui_util.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.h"
+#import "chrome/browser/ui/cocoa/browser_command_executor.h"
+#import "chrome/browser/ui/cocoa/tab_contents_controller.h"
+#import "chrome/browser/ui/cocoa/tab_strip_controller.h"
+#import "chrome/browser/ui/cocoa/tab_window_controller.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#import "chrome/browser/ui/cocoa/url_drop_target.h"
+#import "chrome/browser/ui/cocoa/view_resizer.h"
+
+
+class Browser;
+class BrowserWindow;
+class BrowserWindowCocoa;
+class ConstrainedWindowMac;
+@class DevToolsController;
+@class DownloadShelfController;
+@class FindBarCocoaController;
+@class FullscreenController;
+@class GTMWindowSheetController;
+@class IncognitoImageView;
+@class InfoBarContainerController;
+class LocationBarViewMac;
+@class PreviewableContentsController;
+@class SidebarController;
+class StatusBubbleMac;
+class TabContents;
+@class TabStripController;
+@class TabStripView;
+@class ToolbarController;
+
+
+@interface BrowserWindowController :
+ TabWindowController<NSUserInterfaceValidations,
+ BookmarkBarControllerDelegate,
+ BrowserCommandExecutor,
+ ViewResizer,
+ TabContentsControllerDelegate,
+ TabStripControllerDelegate> {
+ @private
+ // The ordering of these members is important as it determines the order in
+ // which they are destroyed. |browser_| needs to be destroyed last as most of
+ // the other objects hold weak references to it or things it owns
+ // (tab/toolbar/bookmark models, profiles, etc).
+ scoped_ptr<Browser> browser_;
+ NSWindow* savedRegularWindow_;
+ scoped_ptr<BrowserWindowCocoa> windowShim_;
+ scoped_nsobject<ToolbarController> toolbarController_;
+ scoped_nsobject<TabStripController> tabStripController_;
+ scoped_nsobject<FindBarCocoaController> findBarCocoaController_;
+ scoped_nsobject<InfoBarContainerController> infoBarContainerController_;
+ scoped_nsobject<DownloadShelfController> downloadShelfController_;
+ scoped_nsobject<BookmarkBarController> bookmarkBarController_;
+ scoped_nsobject<DevToolsController> devToolsController_;
+ scoped_nsobject<SidebarController> sidebarController_;
+ scoped_nsobject<PreviewableContentsController> previewableContentsController_;
+ scoped_nsobject<FullscreenController> fullscreenController_;
+
+ // Strong. StatusBubble is a special case of a strong reference that
+ // we don't wrap in a scoped_ptr because it is acting the same
+ // as an NSWindowController in that it wraps a window that must
+ // be shut down before our destructors are called.
+ StatusBubbleMac* statusBubble_;
+
+ BookmarkBubbleController* bookmarkBubbleController_; // Weak.
+ BOOL initializing_; // YES while we are currently in initWithBrowser:
+ BOOL ownsBrowser_; // Only ever NO when testing
+
+ // The total amount by which we've grown the window up or down (to display a
+ // bookmark bar and/or download shelf), respectively; reset to 0 when moved
+ // away from the bottom/top or resized (or zoomed).
+ CGFloat windowTopGrowth_;
+ CGFloat windowBottomGrowth_;
+
+ // YES only if we're shrinking the window from an apparent zoomed state (which
+ // we'll only do if we grew it to the zoomed state); needed since we'll then
+ // restrict the amount of shrinking by the amounts specified above. Reset to
+ // NO on growth.
+ BOOL isShrinkingFromZoomed_;
+
+ // The raw accumulated zoom value and the actual zoom increments made for an
+ // an in-progress pinch gesture.
+ CGFloat totalMagnifyGestureAmount_;
+ NSInteger currentZoomStepDelta_;
+
+ // The view which shows the incognito badge (NULL if not an incognito window).
+ // Needed to access the view to move it to/from the fullscreen window.
+ scoped_nsobject<IncognitoImageView> incognitoBadge_;
+
+ // Lazily created view which draws the background for the floating set of bars
+ // in fullscreen mode (for window types having a floating bar; it remains nil
+ // for those which don't).
+ scoped_nsobject<NSView> floatingBarBackingView_;
+
+ // Tracks whether the floating bar is above or below the bookmark bar, in
+ // terms of z-order.
+ BOOL floatingBarAboveBookmarkBar_;
+
+ // The proportion of the floating bar which is shown (in fullscreen mode).
+ CGFloat floatingBarShownFraction_;
+
+ // Various UI elements/events may want to ensure that the floating bar is
+ // visible (in fullscreen mode), e.g., because of where the mouse is or where
+ // keyboard focus is. Whenever an object requires bar visibility, it has
+ // itself added to |barVisibilityLocks_|. When it no longer requires bar
+ // visibility, it has itself removed.
+ scoped_nsobject<NSMutableSet> barVisibilityLocks_;
+
+ // Bar visibility locks and releases only result (when appropriate) in changes
+ // in visible state when the following is |YES|.
+ BOOL barVisibilityUpdatesEnabled_;
+}
+
+// A convenience class method which gets the |BrowserWindowController| for a
+// given window. This method returns nil if no window in the chain has a BWC.
++ (BrowserWindowController*)browserWindowControllerForWindow:(NSWindow*)window;
+
+// A convenience class method which gets the |BrowserWindowController| for a
+// given view. This is the controller for the window containing |view|, if it
+// is a BWC, or the first controller in the parent-window chain that is a
+// BWC. This method returns nil if no window in the chain has a BWC.
++ (BrowserWindowController*)browserWindowControllerForView:(NSView*)view;
+
+// Load the browser window nib and do any Cocoa-specific initialization.
+// Takes ownership of |browser|.
+- (id)initWithBrowser:(Browser*)browser;
+
+// Call to make the browser go away from other places in the cross-platform
+// code.
+- (void)destroyBrowser;
+
+// Access the C++ bridge between the NSWindow and the rest of Chromium.
+- (BrowserWindow*)browserWindow;
+
+// Return a weak pointer to the toolbar controller.
+- (ToolbarController*)toolbarController;
+
+// Return a weak pointer to the tab strip controller.
+- (TabStripController*)tabStripController;
+
+// Access the C++ bridge object representing the status bubble for the window.
+- (StatusBubbleMac*)statusBubble;
+
+// Access the C++ bridge object representing the location bar.
+- (LocationBarViewMac*)locationBarBridge;
+
+// Updates the toolbar (and transitively the location bar) with the states of
+// the specified |tab|. If |shouldRestore| is true, we're switching
+// (back?) to this tab and should restore any previous location bar state
+// (such as user editing) as well.
+- (void)updateToolbarWithContents:(TabContents*)tab
+ shouldRestoreState:(BOOL)shouldRestore;
+
+// Sets whether or not the current page in the frontmost tab is bookmarked.
+- (void)setStarredState:(BOOL)isStarred;
+
+// Called to tell the selected tab to update its loading state.
+// |force| is set if the update is due to changing tabs, as opposed to
+// the page-load finishing. See comment in reload_button.h.
+- (void)setIsLoading:(BOOL)isLoading force:(BOOL)force;
+
+// Brings this controller's window to the front.
+- (void)activate;
+
+// Make the location bar the first responder, if possible.
+- (void)focusLocationBar:(BOOL)selectAll;
+
+// Make the (currently-selected) tab contents the first responder, if possible.
+- (void)focusTabContents;
+
+// Returns the frame of the regular (non-fullscreened) window (even if the
+// window is currently in fullscreen mode). The frame is returned in Cocoa
+// coordinates (origin in bottom-left).
+- (NSRect)regularWindowFrame;
+
+- (BOOL)isBookmarkBarVisible;
+
+// Returns YES if the bookmark bar is currently animating.
+- (BOOL)isBookmarkBarAnimating;
+
+// Called after bookmark bar visibility changes (due to pref change or change in
+// tab/tab contents).
+- (void)updateBookmarkBarVisibilityWithAnimation:(BOOL)animate;
+
+- (BOOL)isDownloadShelfVisible;
+
+// Lazily creates the download shelf in visible state if it doesn't exist yet.
+- (DownloadShelfController*)downloadShelf;
+
+// Retains the given FindBarCocoaController and adds its view to this
+// browser window. Must only be called once per
+// BrowserWindowController.
+- (void)addFindBar:(FindBarCocoaController*)findBarCocoaController;
+
+// The user changed the theme.
+- (void)userChangedTheme;
+
+// Executes the command in the context of the current browser.
+// |command| is an integer value containing one of the constants defined in the
+// "chrome/app/chrome_command_ids.h" file.
+- (void)executeCommand:(int)command;
+
+// Delegate method for the status bubble to query its base frame.
+- (NSRect)statusBubbleBaseFrame;
+
+// Show the bookmark bubble (e.g. user just clicked on the STAR)
+- (void)showBookmarkBubbleForURL:(const GURL&)url
+ alreadyBookmarked:(BOOL)alreadyBookmarked;
+
+// Returns the (lazily created) window sheet controller of this window. Used
+// for the per-tab sheets.
+- (GTMWindowSheetController*)sheetController;
+
+// Requests that |window| is opened as a per-tab sheet to the current tab.
+- (void)attachConstrainedWindow:(ConstrainedWindowMac*)window;
+// Closes the tab sheet |window| and potentially shows the next sheet in the
+// tab's sheet queue.
+- (void)removeConstrainedWindow:(ConstrainedWindowMac*)window;
+// Returns NO if constrained windows cannot be attached to this window.
+- (BOOL)canAttachConstrainedWindow;
+
+// Shows or hides the docked web inspector depending on |contents|'s state.
+- (void)updateDevToolsForContents:(TabContents*)contents;
+
+// Displays the active sidebar linked to the |contents| or hides sidebar UI,
+// if there's no such sidebar.
+- (void)updateSidebarForContents:(TabContents*)contents;
+
+// Gets the current theme provider.
+- (ThemeProvider*)themeProvider;
+
+// Gets the window style.
+- (ThemedWindowStyle)themedWindowStyle;
+
+// Gets the pattern phase for the window.
+- (NSPoint)themePatternPhase;
+
+// Return the point to which a bubble window's arrow should point.
+- (NSPoint)bookmarkBubblePoint;
+
+// Call when the user changes the tab strip display mode, enabling or
+// disabling vertical tabs for this browser. Re-flows the contents of the
+// browser.
+- (void)toggleTabStripDisplayMode;
+
+// Shows or hides the Instant preview contents.
+- (void)showInstant:(TabContents*)previewContents;
+- (void)hideInstant;
+
+// Returns the frame, in Cocoa (unflipped) screen coordinates, of the area where
+// Instant results are. If Instant is not showing, returns the frame of where
+// it would be.
+- (NSRect)instantFrame;
+
+// Called when the Add Search Engine dialog is closed.
+- (void)sheetDidEnd:(NSWindow*)sheet
+ returnCode:(NSInteger)code
+ context:(void*)context;
+
+@end // @interface BrowserWindowController
+
+
+// Methods having to do with the window type (normal/popup/app, and whether the
+// window has various features; fullscreen methods are separate).
+@interface BrowserWindowController(WindowType)
+
+// Determines whether this controller's window supports a given feature (i.e.,
+// whether a given feature is or can be shown in the window).
+// TODO(viettrungluu): |feature| is really should be |Browser::Feature|, but I
+// don't want to include browser.h (and you can't forward declare enums).
+- (BOOL)supportsWindowFeature:(int)feature;
+
+// Called to check whether or not this window has a normal title bar (YES if it
+// does, NO otherwise). (E.g., normal browser windows do not, pop-ups do.)
+- (BOOL)hasTitleBar;
+
+// Called to check whether or not this window has a toolbar (YES if it does, NO
+// otherwise). (E.g., normal browser windows do, pop-ups do not.)
+- (BOOL)hasToolbar;
+
+// Called to check whether or not this window has a location bar (YES if it
+// does, NO otherwise). (E.g., normal browser windows do, pop-ups may or may
+// not.)
+- (BOOL)hasLocationBar;
+
+// Called to check whether or not this window can have bookmark bar (YES if it
+// does, NO otherwise). (E.g., normal browser windows may, pop-ups may not.)
+- (BOOL)supportsBookmarkBar;
+
+// Called to check if this controller's window is a normal window (e.g., not a
+// pop-up window). Returns YES if it is, NO otherwise.
+// Note: The |-has...| methods are usually preferred, so this method is largely
+// deprecated.
+- (BOOL)isNormalWindow;
+
+@end // @interface BrowserWindowController(WindowType)
+
+
+// Methods having to do with fullscreen mode.
+@interface BrowserWindowController(Fullscreen)
+
+// Enters (or exits) fullscreen mode.
+- (void)setFullscreen:(BOOL)fullscreen;
+
+// Returns fullscreen state.
+- (BOOL)isFullscreen;
+
+// Resizes the fullscreen window to fit the screen it's currently on. Called by
+// the FullscreenController when there is a change in monitor placement or
+// resolution.
+- (void)resizeFullscreenWindow;
+
+// Gets or sets the fraction of the floating bar (fullscreen overlay) that is
+// shown. 0 is completely hidden, 1 is fully shown.
+- (CGFloat)floatingBarShownFraction;
+- (void)setFloatingBarShownFraction:(CGFloat)fraction;
+
+// Query/lock/release the requirement that the tab strip/toolbar/attached
+// bookmark bar bar cluster is visible (e.g., when one of its elements has
+// focus). This is required for the floating bar in fullscreen mode, but should
+// also be called when not in fullscreen mode; see the comments for
+// |barVisibilityLocks_| for more details. Double locks/releases by the same
+// owner are ignored. If |animate:| is YES, then an animation may be performed,
+// possibly after a small delay if |delay:| is YES. If |animate:| is NO,
+// |delay:| will be ignored. In the case of multiple calls, later calls have
+// precedence with the rule that |animate:NO| has precedence over |animate:YES|,
+// and |delay:NO| has precedence over |delay:YES|.
+- (BOOL)isBarVisibilityLockedForOwner:(id)owner;
+- (void)lockBarVisibilityForOwner:(id)owner
+ withAnimation:(BOOL)animate
+ delay:(BOOL)delay;
+- (void)releaseBarVisibilityForOwner:(id)owner
+ withAnimation:(BOOL)animate
+ delay:(BOOL)delay;
+
+// Returns YES if any of the views in the floating bar currently has focus.
+- (BOOL)floatingBarHasFocus;
+
+// Opens the tabpose window.
+- (void)openTabpose;
+
+@end // @interface BrowserWindowController(Fullscreen)
+
+
+// Methods which are either only for testing, or only public for testing.
+@interface BrowserWindowController(TestingAPI)
+
+// Put the incognito badge on the browser and adjust the tab strip
+// accordingly.
+- (void)installIncognitoBadge;
+
+// Allows us to initWithBrowser withOUT taking ownership of the browser.
+- (id)initWithBrowser:(Browser*)browser takeOwnership:(BOOL)ownIt;
+
+// Adjusts the window height by the given amount. If the window spans from the
+// top of the current workspace to the bottom of the current workspace, the
+// height is not adjusted. If growing the window by the requested amount would
+// size the window to be taller than the current workspace, the window height is
+// capped to be equal to the height of the current workspace. If the window is
+// partially offscreen, its height is not adjusted at all. This function
+// prefers to grow the window down, but will grow up if needed. Calls to this
+// function should be followed by a call to |layoutSubviews|.
+- (void)adjustWindowHeightBy:(CGFloat)deltaH;
+
+// Return an autoreleased NSWindow suitable for fullscreen use.
+- (NSWindow*)createFullscreenWindow;
+
+// Resets any saved state about window growth (due to showing the bookmark bar
+// or the download shelf), so that future shrinking will occur from the bottom.
+- (void)resetWindowGrowthState;
+
+// Computes by how far in each direction, horizontal and vertical, the
+// |source| rect doesn't fit into |target|.
+- (NSSize)overflowFrom:(NSRect)source
+ to:(NSRect)target;
+@end // @interface BrowserWindowController(TestingAPI)
+
+
+#endif // CHROME_BROWSER_UI_COCOA_BROWSER_WINDOW_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/browser_window_controller.mm b/chrome/browser/ui/cocoa/browser_window_controller.mm
new file mode 100644
index 0000000..82151f0
--- /dev/null
+++ b/chrome/browser/ui/cocoa/browser_window_controller.mm
@@ -0,0 +1,2059 @@
+// 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/ui/cocoa/browser_window_controller.h"
+
+#include <Carbon/Carbon.h>
+
+#include "app/l10n_util.h"
+#include "app/l10n_util_mac.h"
+#include "base/mac_util.h"
+#include "app/mac/scoped_nsdisable_screen_updates.h"
+#include "base/nsimage_cache_mac.h"
+#import "base/scoped_nsobject.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h" // IDC_*
+#include "chrome/browser/bookmarks/bookmark_editor.h"
+#include "chrome/browser/dock_info.h"
+#include "chrome/browser/encoding_menu_controller.h"
+#include "chrome/browser/google/google_util.h"
+#include "chrome/browser/location_bar.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/renderer_host/render_widget_host_view.h"
+#include "chrome/browser/sync/profile_sync_service.h"
+#include "chrome/browser/sync/sync_ui_util_mac.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents/tab_contents_view_mac.h"
+#include "chrome/browser/tab_contents_wrapper.h"
+#include "chrome/browser/tabs/tab_strip_model.h"
+#include "chrome/browser/themes/browser_theme_provider.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list.h"
+#import "chrome/browser/ui/cocoa/background_gradient_view.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h"
+#import "chrome/browser/ui/cocoa/browser_window_cocoa.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller_private.h"
+#import "chrome/browser/ui/cocoa/dev_tools_controller.h"
+#import "chrome/browser/ui/cocoa/download/download_shelf_controller.h"
+#import "chrome/browser/ui/cocoa/event_utils.h"
+#import "chrome/browser/ui/cocoa/fast_resize_view.h"
+#import "chrome/browser/ui/cocoa/find_bar_bridge.h"
+#import "chrome/browser/ui/cocoa/find_bar_cocoa_controller.h"
+#import "chrome/browser/ui/cocoa/focus_tracker.h"
+#import "chrome/browser/ui/cocoa/fullscreen_controller.h"
+#import "chrome/browser/ui/cocoa/fullscreen_window.h"
+#import "chrome/browser/ui/cocoa/infobar_container_controller.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
+#import "chrome/browser/ui/cocoa/previewable_contents_controller.h"
+#import "chrome/browser/ui/cocoa/nswindow_additions.h"
+#import "chrome/browser/ui/cocoa/sad_tab_controller.h"
+#import "chrome/browser/ui/cocoa/sidebar_controller.h"
+#import "chrome/browser/ui/cocoa/status_bubble_mac.h"
+#import "chrome/browser/ui/cocoa/tab_contents_controller.h"
+#import "chrome/browser/ui/cocoa/tab_strip_controller.h"
+#import "chrome/browser/ui/cocoa/tab_strip_view.h"
+#import "chrome/browser/ui/cocoa/tab_view.h"
+#import "chrome/browser/ui/cocoa/tabpose_window.h"
+#import "chrome/browser/ui/cocoa/toolbar_controller.h"
+#include "chrome/browser/window_sizer.h"
+#include "chrome/common/url_constants.h"
+#include "grit/generated_resources.h"
+#include "grit/locale_settings.h"
+
+// ORGANIZATION: This is a big file. It is (in principle) organized as follows
+// (in order):
+// 1. Interfaces. Very short, one-time-use classes may include an implementation
+// immediately after their interface.
+// 2. The general implementation section, ordered as follows:
+// i. Public methods and overrides.
+// ii. Overrides/implementations of undocumented methods.
+// iii. Delegate methods for various protocols, formal and informal, to which
+// |BrowserWindowController| conforms.
+// 3. (temporary) Implementation sections for various categories.
+//
+// Private methods are defined and implemented separately in
+// browser_window_controller_private.{h,mm}.
+//
+// Not all of the above guidelines are followed and more (re-)organization is
+// needed. BUT PLEASE TRY TO KEEP THIS FILE ORGANIZED. I'd rather re-organize as
+// little as possible, since doing so messes up the file's history.
+//
+// TODO(viettrungluu): [crbug.com/35543] on-going re-organization, splitting
+// things into multiple files -- the plan is as follows:
+// - in general, everything stays in browser_window_controller.h, but is split
+// off into categories (see below)
+// - core stuff stays in browser_window_controller.mm
+// - ... overrides also stay (without going into a category, in particular)
+// - private stuff which everyone needs goes into
+// browser_window_controller_private.{h,mm}; if no one else needs them, they
+// can go in individual files (see below)
+// - area/task-specific stuff go in browser_window_controller_<area>.mm
+// - ... in categories called "(<Area>)" or "(<PrivateArea>)"
+// Plan of action:
+// - first re-organize into categories
+// - then split into files
+
+// Notes on self-inflicted (not user-inflicted) window resizing and moving:
+//
+// When the bookmark bar goes from hidden to shown (on a non-NTP) page, or when
+// the download shelf goes from hidden to shown, we grow the window downwards in
+// order to maintain a constant content area size. When either goes from shown
+// to hidden, we consequently shrink the window from the bottom, also to keep
+// the content area size constant. To keep things simple, if the window is not
+// entirely on-screen, we don't grow/shrink the window.
+//
+// The complications come in when there isn't enough room (on screen) below the
+// window to accomodate the growth. In this case, we grow the window first
+// downwards, and then upwards. So, when it comes to shrinking, we do the
+// opposite: shrink from the top by the amount by which we grew at the top, and
+// then from the bottom -- unless the user moved/resized/zoomed the window, in
+// which case we "reset state" and just shrink from the bottom.
+//
+// A further complication arises due to the way in which "zoom" ("maximize")
+// works on Mac OS X. Basically, for our purposes, a window is "zoomed" whenever
+// it occupies the full available vertical space. (Note that the green zoom
+// button does not track zoom/unzoomed state per se, but basically relies on
+// this heuristic.) We don't, in general, want to shrink the window if the
+// window is zoomed (scenario: window is zoomed, download shelf opens -- which
+// doesn't cause window growth, download shelf closes -- shouldn't cause the
+// window to become unzoomed!). However, if we grew the window
+// (upwards/downwards) to become zoomed in the first place, we *should* shrink
+// the window by the amounts by which we grew (scenario: window occupies *most*
+// of vertical space, download shelf opens causing growth so that window
+// occupies all of vertical space -- i.e., window is effectively zoomed,
+// download shelf closes -- should return the window to its previous state).
+//
+// A major complication is caused by the way grows/shrinks are handled and
+// animated. Basically, the BWC doesn't see the global picture, but it sees
+// grows and shrinks in small increments (as dictated by the animation). Thus
+// window growth/shrinkage (at the top/bottom) have to be tracked incrementally.
+// Allowing shrinking from the zoomed state also requires tracking: We check on
+// any shrink whether we're both zoomed and have previously grown -- if so, we
+// set a flag, and constrain any resize by the allowed amounts. On further
+// shrinks, we check the flag (since the size/position of the window will no
+// longer indicate that the window is shrinking from an apparent zoomed state)
+// and if it's set we continue to constrain the resize.
+
+
+@interface NSWindow(NSPrivateApis)
+// Note: These functions are private, use -[NSObject respondsToSelector:]
+// before calling them.
+
+- (void)setBottomCornerRounded:(BOOL)rounded;
+
+- (NSRect)_growBoxRect;
+
+@end
+
+
+// IncognitoImageView subclasses NSImageView to allow mouse events to pass
+// through it so you can drag the window by dragging on the spy guy
+@interface IncognitoImageView : NSImageView
+@end
+
+@implementation IncognitoImageView
+- (BOOL)mouseDownCanMoveWindow {
+ return YES;
+}
+@end
+
+
+@implementation BrowserWindowController
+
++ (BrowserWindowController*)browserWindowControllerForWindow:(NSWindow*)window {
+ while (window) {
+ id controller = [window windowController];
+ if ([controller isKindOfClass:[BrowserWindowController class]])
+ return (BrowserWindowController*)controller;
+ window = [window parentWindow];
+ }
+ return nil;
+}
+
++ (BrowserWindowController*)browserWindowControllerForView:(NSView*)view {
+ NSWindow* window = [view window];
+ return [BrowserWindowController browserWindowControllerForWindow:window];
+}
+
+// Load the browser window nib and do any Cocoa-specific initialization.
+// Takes ownership of |browser|. Note that the nib also sets this controller
+// up as the window's delegate.
+- (id)initWithBrowser:(Browser*)browser {
+ return [self initWithBrowser:browser takeOwnership:YES];
+}
+
+// Private(TestingAPI) init routine with testing options.
+- (id)initWithBrowser:(Browser*)browser takeOwnership:(BOOL)ownIt {
+ // Use initWithWindowNibPath:: instead of initWithWindowNibName: so we
+ // can override it in a unit test.
+ NSString* nibpath = [mac_util::MainAppBundle()
+ pathForResource:@"BrowserWindow"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ DCHECK(browser);
+ initializing_ = YES;
+ browser_.reset(browser);
+ ownsBrowser_ = ownIt;
+ NSWindow* window = [self window];
+ windowShim_.reset(new BrowserWindowCocoa(browser, self, window));
+
+ // Create the bar visibility lock set; 10 is arbitrary, but should hopefully
+ // be big enough to hold all locks that'll ever be needed.
+ barVisibilityLocks_.reset([[NSMutableSet setWithCapacity:10] retain]);
+
+ // Sets the window to not have rounded corners, which prevents
+ // the resize control from being inset slightly and looking ugly.
+ if ([window respondsToSelector:@selector(setBottomCornerRounded:)])
+ [window setBottomCornerRounded:NO];
+
+ // Get the most appropriate size for the window, then enforce the
+ // minimum width and height. The window shim will handle flipping
+ // the coordinates for us so we can use it to save some code.
+ // Note that this may leave a significant portion of the window
+ // offscreen, but there will always be enough window onscreen to
+ // drag the whole window back into view.
+ NSSize minSize = [[self window] minSize];
+ gfx::Rect desiredContentRect = browser_->GetSavedWindowBounds();
+ gfx::Rect windowRect = desiredContentRect;
+ if (windowRect.width() < minSize.width)
+ windowRect.set_width(minSize.width);
+ if (windowRect.height() < minSize.height)
+ windowRect.set_height(minSize.height);
+
+ // When we are given x/y coordinates of 0 on a created popup window, assume
+ // none were given by the window.open() command.
+ if (browser_->type() & Browser::TYPE_POPUP &&
+ windowRect.x() == 0 && windowRect.y() == 0) {
+ gfx::Size size = windowRect.size();
+ windowRect.set_origin(WindowSizer::GetDefaultPopupOrigin(size));
+ }
+
+ // Size and position the window. Note that it is not yet onscreen. Popup
+ // windows may get resized later on in this function, once the actual size
+ // of the toolbar/tabstrip is known.
+ windowShim_->SetBounds(windowRect);
+
+ // Puts the incognito badge on the window frame, if necessary.
+ [self installIncognitoBadge];
+
+ // Create a sub-controller for the docked devTools and add its view to the
+ // hierarchy. This must happen before the sidebar controller is
+ // instantiated.
+ devToolsController_.reset(
+ [[DevToolsController alloc] initWithDelegate:self]);
+ [[devToolsController_ view] setFrame:[[self tabContentArea] bounds]];
+ [[self tabContentArea] addSubview:[devToolsController_ view]];
+
+ // Create a sub-controller for the docked sidebar and add its view to the
+ // hierarchy. This must happen before the previewable contents controller
+ // is instantiated.
+ sidebarController_.reset([[SidebarController alloc] initWithDelegate:self]);
+ [[sidebarController_ view] setFrame:[[devToolsController_ view] bounds]];
+ [[devToolsController_ view] addSubview:[sidebarController_ view]];
+
+ // Create the previewable contents controller. This provides the switch
+ // view that TabStripController needs.
+ previewableContentsController_.reset(
+ [[PreviewableContentsController alloc] init]);
+ [[previewableContentsController_ view]
+ setFrame:[[sidebarController_ view] bounds]];
+ [[sidebarController_ view]
+ addSubview:[previewableContentsController_ view]];
+
+ // Create a controller for the tab strip, giving it the model object for
+ // this window's Browser and the tab strip view. The controller will handle
+ // registering for the appropriate tab notifications from the back-end and
+ // managing the creation of new tabs.
+ [self createTabStripController];
+
+ // Create the infobar container view, so we can pass it to the
+ // ToolbarController.
+ infoBarContainerController_.reset(
+ [[InfoBarContainerController alloc] initWithResizeDelegate:self]);
+ [[[self window] contentView] addSubview:[infoBarContainerController_ view]];
+
+ // Create a controller for the toolbar, giving it the toolbar model object
+ // and the toolbar view from the nib. The controller will handle
+ // registering for the appropriate command state changes from the back-end.
+ // Adds the toolbar to the content area.
+ toolbarController_.reset([[ToolbarController alloc]
+ initWithModel:browser->toolbar_model()
+ commands:browser->command_updater()
+ profile:browser->profile()
+ browser:browser
+ resizeDelegate:self]);
+ [toolbarController_ setHasToolbar:[self hasToolbar]
+ hasLocationBar:[self hasLocationBar]];
+ [[[self window] contentView] addSubview:[toolbarController_ view]];
+
+ // Create a sub-controller for the bookmark bar.
+ bookmarkBarController_.reset(
+ [[BookmarkBarController alloc]
+ initWithBrowser:browser_.get()
+ initialWidth:NSWidth([[[self window] contentView] frame])
+ delegate:self
+ resizeDelegate:self]);
+
+ // Add bookmark bar to the view hierarchy, which also triggers the nib load.
+ // The bookmark bar is defined (in the nib) to be bottom-aligned to its
+ // parent view (among other things), so position and resize properties don't
+ // need to be set.
+ [[[self window] contentView] addSubview:[bookmarkBarController_ view]
+ positioned:NSWindowBelow
+ relativeTo:[toolbarController_ view]];
+ [bookmarkBarController_ setBookmarkBarEnabled:[self supportsBookmarkBar]];
+
+ // We don't want to try and show the bar before it gets placed in its parent
+ // view, so this step shoudn't be inside the bookmark bar controller's
+ // |-awakeFromNib|.
+ [self updateBookmarkBarVisibilityWithAnimation:NO];
+
+ // Allow bar visibility to be changed.
+ [self enableBarVisibilityUpdates];
+
+ // Force a relayout of all the various bars.
+ [self layoutSubviews];
+
+ // For a popup window, |desiredContentRect| contains the desired height of
+ // the content, not of the whole window. Now that all the views are laid
+ // out, measure the current content area size and grow if needed. The
+ // window has not been placed onscreen yet, so this extra resize will not
+ // cause visible jank.
+ if (browser_->type() & Browser::TYPE_POPUP) {
+ CGFloat deltaH = desiredContentRect.height() -
+ NSHeight([[self tabContentArea] frame]);
+ // Do not shrink the window, as that may break minimum size invariants.
+ if (deltaH > 0) {
+ // Convert from tabContentArea coordinates to window coordinates.
+ NSSize convertedSize =
+ [[self tabContentArea] convertSize:NSMakeSize(0, deltaH)
+ toView:nil];
+ NSRect frame = [[self window] frame];
+ frame.size.height += convertedSize.height;
+ frame.origin.y -= convertedSize.height;
+ [[self window] setFrame:frame display:NO];
+ }
+ }
+
+ // Create the bridge for the status bubble.
+ statusBubble_ = new StatusBubbleMac([self window], self);
+
+ // Register for application hide/unhide notifications.
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(applicationDidHide:)
+ name:NSApplicationDidHideNotification
+ object:nil];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(applicationDidUnhide:)
+ name:NSApplicationDidUnhideNotification
+ object:nil];
+
+ // This must be done after the view is added to the window since it relies
+ // on the window bounds to determine whether to show buttons or not.
+ if ([self hasToolbar]) // Do not create the buttons in popups.
+ [toolbarController_ createBrowserActionButtons];
+
+ // We are done initializing now.
+ initializing_ = NO;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ browser_->CloseAllTabs();
+ [downloadShelfController_ exiting];
+
+ // Explicitly release |fullscreenController_| here, as it may call back to
+ // this BWC in |-dealloc|. We are required to call |-exitFullscreen| before
+ // releasing the controller.
+ [fullscreenController_ exitFullscreen];
+ fullscreenController_.reset();
+
+ // Under certain testing configurations we may not actually own the browser.
+ if (ownsBrowser_ == NO)
+ ignore_result(browser_.release());
+
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+
+ [super dealloc];
+}
+
+- (BrowserWindow*)browserWindow {
+ return windowShim_.get();
+}
+
+- (ToolbarController*)toolbarController {
+ return toolbarController_.get();
+}
+
+- (TabStripController*)tabStripController {
+ return tabStripController_.get();
+}
+
+- (StatusBubbleMac*)statusBubble {
+ return statusBubble_;
+}
+
+- (LocationBarViewMac*)locationBarBridge {
+ return [toolbarController_ locationBarBridge];
+}
+
+- (void)destroyBrowser {
+ [NSApp removeWindowsItem:[self window]];
+
+ // We need the window to go away now.
+ // We can't actually use |-autorelease| here because there's an embedded
+ // run loop in the |-performClose:| which contains its own autorelease pool.
+ // Instead call it after a zero-length delay, which gets us back to the main
+ // event loop.
+ [self performSelector:@selector(autorelease)
+ withObject:nil
+ afterDelay:0];
+}
+
+// Called when the window meets the criteria to be closed (ie,
+// |-windowShouldClose:| returns YES). We must be careful to preserve the
+// semantics of BrowserWindow::Close() and not call the Browser's dtor directly
+// from this method.
+- (void)windowWillClose:(NSNotification*)notification {
+ DCHECK_EQ([notification object], [self window]);
+ DCHECK(browser_->tabstrip_model()->empty());
+ [savedRegularWindow_ close];
+ // We delete statusBubble here because we need to kill off the dependency
+ // that its window has on our window before our window goes away.
+ delete statusBubble_;
+ statusBubble_ = NULL;
+ // We can't actually use |-autorelease| here because there's an embedded
+ // run loop in the |-performClose:| which contains its own autorelease pool.
+ // Instead call it after a zero-length delay, which gets us back to the main
+ // event loop.
+ [self performSelector:@selector(autorelease)
+ withObject:nil
+ afterDelay:0];
+}
+
+- (void)attachConstrainedWindow:(ConstrainedWindowMac*)window {
+ [tabStripController_ attachConstrainedWindow:window];
+}
+
+- (void)removeConstrainedWindow:(ConstrainedWindowMac*)window {
+ [tabStripController_ removeConstrainedWindow:window];
+}
+
+- (BOOL)canAttachConstrainedWindow {
+ return ![previewableContentsController_ isShowingPreview];
+}
+
+- (void)updateDevToolsForContents:(TabContents*)contents {
+ [devToolsController_ updateDevToolsForTabContents:contents];
+ [devToolsController_ ensureContentsVisible];
+}
+
+- (void)updateSidebarForContents:(TabContents*)contents {
+ [sidebarController_ updateSidebarForTabContents:contents];
+ [sidebarController_ ensureContentsVisible];
+}
+
+// Called when the user wants to close a window or from the shutdown process.
+// The Browser object is in control of whether or not we're allowed to close. It
+// may defer closing due to several states, such as onUnload handlers needing to
+// be fired. If closing is deferred, the Browser will handle the processing
+// required to get us to the closing state and (by watching for all the tabs
+// going away) will again call to close the window when it's finally ready.
+- (BOOL)windowShouldClose:(id)sender {
+ // Disable updates while closing all tabs to avoid flickering.
+ app::mac::ScopedNSDisableScreenUpdates disabler;
+ // Give beforeunload handlers the chance to cancel the close before we hide
+ // the window below.
+ if (!browser_->ShouldCloseWindow())
+ return NO;
+
+ // saveWindowPositionIfNeeded: only works if we are the last active
+ // window, but orderOut: ends up activating another window, so we
+ // have to save the window position before we call orderOut:.
+ [self saveWindowPositionIfNeeded];
+
+ if (!browser_->tabstrip_model()->empty()) {
+ // Tab strip isn't empty. Hide the frame (so it appears to have closed
+ // immediately) and close all the tabs, allowing the renderers to shut
+ // down. When the tab strip is empty we'll be called back again.
+ [[self window] orderOut:self];
+ browser_->OnWindowClosing();
+ return NO;
+ }
+
+ // the tab strip is empty, it's ok to close the window
+ return YES;
+}
+
+// Called right after our window became the main window.
+- (void)windowDidBecomeMain:(NSNotification*)notification {
+ BrowserList::SetLastActive(browser_.get());
+ [self saveWindowPositionIfNeeded];
+
+ // TODO(dmaclach): Instead of redrawing the whole window, views that care
+ // about the active window state should be registering for notifications.
+ [[self window] setViewsNeedDisplay:YES];
+
+ // TODO(viettrungluu): For some reason, the above doesn't suffice.
+ if ([self isFullscreen])
+ [floatingBarBackingView_ setNeedsDisplay:YES]; // Okay even if nil.
+}
+
+- (void)windowDidResignMain:(NSNotification*)notification {
+ // TODO(dmaclach): Instead of redrawing the whole window, views that care
+ // about the active window state should be registering for notifications.
+ [[self window] setViewsNeedDisplay:YES];
+
+ // TODO(viettrungluu): For some reason, the above doesn't suffice.
+ if ([self isFullscreen])
+ [floatingBarBackingView_ setNeedsDisplay:YES]; // Okay even if nil.
+}
+
+// Called when we are activated (when we gain focus).
+- (void)windowDidBecomeKey:(NSNotification*)notification {
+ // We need to activate the controls (in the "WebView"). To do this, get the
+ // selected TabContents's RenderWidgetHostViewMac and tell it to activate.
+ if (TabContents* contents = browser_->GetSelectedTabContents()) {
+ if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView())
+ rwhv->SetActive(true);
+ }
+}
+
+// Called when we are deactivated (when we lose focus).
+- (void)windowDidResignKey:(NSNotification*)notification {
+ // If our app is still active and we're still the key window, ignore this
+ // message, since it just means that a menu extra (on the "system status bar")
+ // was activated; we'll get another |-windowDidResignKey| if we ever really
+ // lose key window status.
+ if ([NSApp isActive] && ([NSApp keyWindow] == [self window]))
+ return;
+
+ // We need to deactivate the controls (in the "WebView"). To do this, get the
+ // selected TabContents's RenderWidgetHostView and tell it to deactivate.
+ if (TabContents* contents = browser_->GetSelectedTabContents()) {
+ if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView())
+ rwhv->SetActive(false);
+ }
+}
+
+// Called when we have been minimized.
+- (void)windowDidMiniaturize:(NSNotification *)notification {
+ // Let the selected RenderWidgetHostView know, so that it can tell plugins.
+ if (TabContents* contents = browser_->GetSelectedTabContents()) {
+ if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView())
+ rwhv->SetWindowVisibility(false);
+ }
+}
+
+// Called when we have been unminimized.
+- (void)windowDidDeminiaturize:(NSNotification *)notification {
+ // Let the selected RenderWidgetHostView know, so that it can tell plugins.
+ if (TabContents* contents = browser_->GetSelectedTabContents()) {
+ if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView())
+ rwhv->SetWindowVisibility(true);
+ }
+}
+
+// Called when the application has been hidden.
+- (void)applicationDidHide:(NSNotification *)notification {
+ // Let the selected RenderWidgetHostView know, so that it can tell plugins
+ // (unless we are minimized, in which case nothing has really changed).
+ if (![[self window] isMiniaturized]) {
+ if (TabContents* contents = browser_->GetSelectedTabContents()) {
+ if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView())
+ rwhv->SetWindowVisibility(false);
+ }
+ }
+}
+
+// Called when the application has been unhidden.
+- (void)applicationDidUnhide:(NSNotification *)notification {
+ // Let the selected RenderWidgetHostView know, so that it can tell plugins
+ // (unless we are minimized, in which case nothing has really changed).
+ if (![[self window] isMiniaturized]) {
+ if (TabContents* contents = browser_->GetSelectedTabContents()) {
+ if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView())
+ rwhv->SetWindowVisibility(true);
+ }
+ }
+}
+
+// Called when the user clicks the zoom button (or selects it from the Window
+// menu) to determine the "standard size" of the window, based on the content
+// and other factors. If the current size/location differs nontrivally from the
+// standard size, Cocoa resizes the window to the standard size, and saves the
+// current size as the "user size". If the current size/location is the same (up
+// to a fudge factor) as the standard size, Cocoa resizes the window to the
+// saved user size. (It is possible for the two to coincide.) In this way, the
+// zoom button acts as a toggle. We determine the standard size based on the
+// content, but enforce a minimum width (calculated using the dimensions of the
+// screen) to ensure websites with small intrinsic width (such as google.com)
+// don't end up with a wee window. Moreover, we always declare the standard
+// width to be at least as big as the current width, i.e., we never want zooming
+// to the standard width to shrink the window. This is consistent with other
+// browsers' behaviour, and is desirable in multi-tab situations. Note, however,
+// that the "toggle" behaviour means that the window can still be "unzoomed" to
+// the user size.
+- (NSRect)windowWillUseStandardFrame:(NSWindow*)window
+ defaultFrame:(NSRect)frame {
+ // Forget that we grew the window up (if we in fact did).
+ [self resetWindowGrowthState];
+
+ // |frame| already fills the current screen. Never touch y and height since we
+ // always want to fill vertically.
+
+ // If the shift key is down, maximize. Hopefully this should make the
+ // "switchers" happy.
+ if ([[NSApp currentEvent] modifierFlags] & NSShiftKeyMask) {
+ return frame;
+ }
+
+ // To prevent strange results on portrait displays, the basic minimum zoomed
+ // width is the larger of: 60% of available width, 60% of available height
+ // (bounded by available width).
+ const CGFloat kProportion = 0.6;
+ CGFloat zoomedWidth =
+ std::max(kProportion * frame.size.width,
+ std::min(kProportion * frame.size.height, frame.size.width));
+
+ TabContents* contents = browser_->GetSelectedTabContents();
+ if (contents) {
+ // If the intrinsic width is bigger, then make it the zoomed width.
+ const int kScrollbarWidth = 16; // TODO(viettrungluu): ugh.
+ TabContentsViewMac* tab_contents_view =
+ static_cast<TabContentsViewMac*>(contents->view());
+ CGFloat intrinsicWidth = static_cast<CGFloat>(
+ tab_contents_view->preferred_width() + kScrollbarWidth);
+ zoomedWidth = std::max(zoomedWidth,
+ std::min(intrinsicWidth, frame.size.width));
+ }
+
+ // Never shrink from the current size on zoom (see above).
+ NSRect currentFrame = [[self window] frame];
+ zoomedWidth = std::max(zoomedWidth, currentFrame.size.width);
+
+ // |frame| determines our maximum extents. We need to set the origin of the
+ // frame -- and only move it left if necessary.
+ if (currentFrame.origin.x + zoomedWidth > frame.origin.x + frame.size.width)
+ frame.origin.x = frame.origin.x + frame.size.width - zoomedWidth;
+ else
+ frame.origin.x = currentFrame.origin.x;
+
+ // Set the width. Don't touch y or height.
+ frame.size.width = zoomedWidth;
+
+ return frame;
+}
+
+- (void)activate {
+ [[self window] makeKeyAndOrderFront:self];
+}
+
+// Determine whether we should let a window zoom/unzoom to the given |newFrame|.
+// We avoid letting unzoom move windows between screens, because it's really
+// strange and unintuitive.
+- (BOOL)windowShouldZoom:(NSWindow*)window toFrame:(NSRect)newFrame {
+ // Figure out which screen |newFrame| is on.
+ NSScreen* newScreen = nil;
+ CGFloat newScreenOverlapArea = 0.0;
+ for (NSScreen* screen in [NSScreen screens]) {
+ NSRect overlap = NSIntersectionRect(newFrame, [screen frame]);
+ CGFloat overlapArea = overlap.size.width * overlap.size.height;
+ if (overlapArea > newScreenOverlapArea) {
+ newScreen = screen;
+ newScreenOverlapArea = overlapArea;
+ }
+ }
+ // If we're somehow not on any screen, allow the zoom.
+ if (!newScreen)
+ return YES;
+
+ // If the new screen is the current screen, we can return a definitive YES.
+ // Note: This check is not strictly necessary, but just short-circuits in the
+ // "no-brainer" case. To test the complicated logic below, comment this out!
+ NSScreen* curScreen = [window screen];
+ if (newScreen == curScreen)
+ return YES;
+
+ // Worry a little: What happens when a window is on two (or more) screens?
+ // E.g., what happens in a 50-50 scenario? Cocoa may reasonably elect to zoom
+ // to the other screen rather than staying on the officially current one. So
+ // we compare overlaps with the current window frame, and see if Cocoa's
+ // choice was reasonable (allowing a small rounding error). This should
+ // hopefully avoid us ever erroneously denying a zoom when a window is on
+ // multiple screens.
+ NSRect curFrame = [window frame];
+ NSRect newScrIntersectCurFr = NSIntersectionRect([newScreen frame], curFrame);
+ NSRect curScrIntersectCurFr = NSIntersectionRect([curScreen frame], curFrame);
+ if (newScrIntersectCurFr.size.width*newScrIntersectCurFr.size.height >=
+ (curScrIntersectCurFr.size.width*curScrIntersectCurFr.size.height - 1.0))
+ return YES;
+
+ // If it wasn't reasonable, return NO.
+ return NO;
+}
+
+// Adjusts the window height by the given amount.
+- (void)adjustWindowHeightBy:(CGFloat)deltaH {
+ // By not adjusting the window height when initializing, we can ensure that
+ // the window opens with the same size that was saved on close.
+ if (initializing_ || [self isFullscreen] || deltaH == 0)
+ return;
+
+ NSWindow* window = [self window];
+ NSRect windowFrame = [window frame];
+ NSRect workarea = [[window screen] visibleFrame];
+
+ // If the window is not already fully in the workarea, do not adjust its frame
+ // at all.
+ if (!NSContainsRect(workarea, windowFrame))
+ return;
+
+ // Record the position of the top/bottom of the window, so we can easily check
+ // whether we grew the window upwards/downwards.
+ CGFloat oldWindowMaxY = NSMaxY(windowFrame);
+ CGFloat oldWindowMinY = NSMinY(windowFrame);
+
+ // We are "zoomed" if we occupy the full vertical space.
+ bool isZoomed = (windowFrame.origin.y == workarea.origin.y &&
+ windowFrame.size.height == workarea.size.height);
+
+ // If we're shrinking the window....
+ if (deltaH < 0) {
+ bool didChange = false;
+
+ // Don't reset if not currently zoomed since shrinking can take several
+ // steps!
+ if (isZoomed)
+ isShrinkingFromZoomed_ = YES;
+
+ // If we previously grew at the top, shrink as much as allowed at the top
+ // first.
+ if (windowTopGrowth_ > 0) {
+ CGFloat shrinkAtTopBy = MIN(-deltaH, windowTopGrowth_);
+ windowFrame.size.height -= shrinkAtTopBy; // Shrink the window.
+ deltaH += shrinkAtTopBy; // Update the amount left to shrink.
+ windowTopGrowth_ -= shrinkAtTopBy; // Update the growth state.
+ didChange = true;
+ }
+
+ // Similarly for the bottom (not an "else if" since we may have to
+ // simultaneously shrink at both the top and at the bottom). Note that
+ // |deltaH| may no longer be nonzero due to the above.
+ if (deltaH < 0 && windowBottomGrowth_ > 0) {
+ CGFloat shrinkAtBottomBy = MIN(-deltaH, windowBottomGrowth_);
+ windowFrame.origin.y += shrinkAtBottomBy; // Move the window up.
+ windowFrame.size.height -= shrinkAtBottomBy; // Shrink the window.
+ deltaH += shrinkAtBottomBy; // Update the amount left....
+ windowBottomGrowth_ -= shrinkAtBottomBy; // Update the growth state.
+ didChange = true;
+ }
+
+ // If we're shrinking from zoomed but we didn't change the top or bottom
+ // (since we've reached the limits imposed by |window...Growth_|), then stop
+ // here. Don't reset |isShrinkingFromZoomed_| since we might get called
+ // again for the same shrink.
+ if (isShrinkingFromZoomed_ && !didChange)
+ return;
+ } else {
+ isShrinkingFromZoomed_ = NO;
+
+ // Don't bother with anything else.
+ if (isZoomed)
+ return;
+ }
+
+ // Shrinking from zoomed is handled above (and is constrained by
+ // |window...Growth_|).
+ if (!isShrinkingFromZoomed_) {
+ // Resize the window down until it hits the bottom of the workarea, then if
+ // needed continue resizing upwards. Do not resize the window to be taller
+ // than the current workarea.
+ // Resize the window as requested, keeping the top left corner fixed.
+ windowFrame.origin.y -= deltaH;
+ windowFrame.size.height += deltaH;
+
+ // If the bottom left corner is now outside the visible frame, move the
+ // window up to make it fit, but make sure not to move the top left corner
+ // out of the visible frame.
+ if (windowFrame.origin.y < workarea.origin.y) {
+ windowFrame.origin.y = workarea.origin.y;
+ windowFrame.size.height =
+ std::min(windowFrame.size.height, workarea.size.height);
+ }
+
+ // Record (if applicable) how much we grew the window in either direction.
+ // (N.B.: These only record growth, not shrinkage.)
+ if (NSMaxY(windowFrame) > oldWindowMaxY)
+ windowTopGrowth_ += NSMaxY(windowFrame) - oldWindowMaxY;
+ if (NSMinY(windowFrame) < oldWindowMinY)
+ windowBottomGrowth_ += oldWindowMinY - NSMinY(windowFrame);
+ }
+
+ // Disable subview resizing while resizing the window, or else we will get
+ // unwanted renderer resizes. The calling code must call layoutSubviews to
+ // make things right again.
+ NSView* contentView = [window contentView];
+ [contentView setAutoresizesSubviews:NO];
+ [window setFrame:windowFrame display:NO];
+ [contentView setAutoresizesSubviews:YES];
+}
+
+// Main method to resize browser window subviews. This method should be called
+// when resizing any child of the content view, rather than resizing the views
+// directly. If the view is already the correct height, does not force a
+// relayout.
+- (void)resizeView:(NSView*)view newHeight:(CGFloat)height {
+ // We should only ever be called for one of the following four views.
+ // |downloadShelfController_| may be nil. If we are asked to size the bookmark
+ // bar directly, its superview must be this controller's content view.
+ DCHECK(view);
+ DCHECK(view == [toolbarController_ view] ||
+ view == [infoBarContainerController_ view] ||
+ view == [downloadShelfController_ view] ||
+ view == [bookmarkBarController_ view]);
+
+ // Change the height of the view and call |-layoutSubViews|. We set the height
+ // here without regard to where the view is on the screen or whether it needs
+ // to "grow up" or "grow down." The below call to |-layoutSubviews| will
+ // position each view correctly.
+ NSRect frame = [view frame];
+ if (NSHeight(frame) == height)
+ return;
+
+ // Grow or shrink the window by the amount of the height change. We adjust
+ // the window height only in two cases:
+ // 1) We are adjusting the height of the bookmark bar and it is currently
+ // animating either open or closed.
+ // 2) We are adjusting the height of the download shelf.
+ //
+ // We do not adjust the window height for bookmark bar changes on the NTP.
+ BOOL shouldAdjustBookmarkHeight =
+ [bookmarkBarController_ isAnimatingBetweenState:bookmarks::kHiddenState
+ andState:bookmarks::kShowingState];
+ if ((shouldAdjustBookmarkHeight && view == [bookmarkBarController_ view]) ||
+ view == [downloadShelfController_ view]) {
+ [[self window] disableScreenUpdatesUntilFlush];
+ CGFloat deltaH = height - frame.size.height;
+ [self adjustWindowHeightBy:deltaH];
+ }
+
+ frame.size.height = height;
+ // TODO(rohitrao): Determine if calling setFrame: twice is bad.
+ [view setFrame:frame];
+ [self layoutSubviews];
+}
+
+- (void)setAnimationInProgress:(BOOL)inProgress {
+ [[self tabContentArea] setFastResizeMode:inProgress];
+}
+
+// Update a toggle state for an NSMenuItem if modified.
+// Take care to ensure |item| looks like a NSMenuItem.
+// Called by validateUserInterfaceItem:.
+- (void)updateToggleStateWithTag:(NSInteger)tag forItem:(id)item {
+ if (![item respondsToSelector:@selector(state)] ||
+ ![item respondsToSelector:@selector(setState:)])
+ return;
+
+ // On Windows this logic happens in bookmark_bar_view.cc. On the
+ // Mac we're a lot more MVC happy so we've moved it into a
+ // controller. To be clear, this simply updates the menu item; it
+ // does not display the bookmark bar itself.
+ if (tag == IDC_SHOW_BOOKMARK_BAR) {
+ bool toggled = windowShim_->IsBookmarkBarVisible();
+ NSInteger oldState = [item state];
+ NSInteger newState = toggled ? NSOnState : NSOffState;
+ if (oldState != newState)
+ [item setState:newState];
+ }
+
+ // Update the checked/Unchecked state of items in the encoding menu.
+ // On Windows, this logic is part of |EncodingMenuModel| in
+ // browser/views/toolbar_view.h.
+ EncodingMenuController encoding_controller;
+ if (encoding_controller.DoesCommandBelongToEncodingMenu(tag)) {
+ DCHECK(browser_.get());
+ Profile* profile = browser_->profile();
+ DCHECK(profile);
+ TabContents* current_tab = browser_->GetSelectedTabContents();
+ if (!current_tab) {
+ return;
+ }
+ const std::string encoding = current_tab->encoding();
+
+ bool toggled = encoding_controller.IsItemChecked(profile, encoding, tag);
+ NSInteger oldState = [item state];
+ NSInteger newState = toggled ? NSOnState : NSOffState;
+ if (oldState != newState)
+ [item setState:newState];
+ }
+}
+
+- (BOOL)supportsFullscreen {
+ // TODO(avi, thakis): GTMWindowSheetController has no api to move
+ // tabsheets between windows. Until then, we have to prevent having to
+ // move a tabsheet between windows, e.g. no fullscreen toggling
+ NSArray* a = [[tabStripController_ sheetController] viewsWithAttachedSheets];
+ return [a count] == 0;
+}
+
+// Called to validate menu and toolbar items when this window is key. All the
+// items we care about have been set with the |-commandDispatch:| or
+// |-commandDispatchUsingKeyModifiers:| actions and a target of FirstResponder
+// in IB. If it's not one of those, let it continue up the responder chain to be
+// handled elsewhere. We pull out the tag as the cross-platform constant to
+// differentiate and dispatch the various commands.
+// NOTE: we might have to handle state for app-wide menu items,
+// although we could cheat and directly ask the app controller if our
+// command_updater doesn't support the command. This may or may not be an issue,
+// too early to tell.
+- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
+ SEL action = [item action];
+ BOOL enable = NO;
+ if (action == @selector(commandDispatch:) ||
+ action == @selector(commandDispatchUsingKeyModifiers:)) {
+ NSInteger tag = [item tag];
+ if (browser_->command_updater()->SupportsCommand(tag)) {
+ // Generate return value (enabled state)
+ enable = browser_->command_updater()->IsCommandEnabled(tag);
+ switch (tag) {
+ case IDC_CLOSE_TAB:
+ // Disable "close tab" if we're not the key window or if there's only
+ // one tab.
+ enable &= [self numberOfTabs] > 1 && [[self window] isKeyWindow];
+ break;
+ case IDC_FULLSCREEN: {
+ enable &= [self supportsFullscreen];
+ if ([static_cast<NSObject*>(item) isKindOfClass:[NSMenuItem class]]) {
+ NSString* menuTitle = l10n_util::GetNSString(
+ [self isFullscreen] ? IDS_EXIT_FULLSCREEN_MAC :
+ IDS_ENTER_FULLSCREEN_MAC);
+ [static_cast<NSMenuItem*>(item) setTitle:menuTitle];
+ }
+ break;
+ }
+ case IDC_SYNC_BOOKMARKS:
+ enable &= browser_->profile()->IsSyncAccessible();
+ sync_ui_util::UpdateSyncItem(item, enable, browser_->profile());
+ break;
+ default:
+ // Special handling for the contents of the Text Encoding submenu. On
+ // Mac OS, instead of enabling/disabling the top-level menu item, we
+ // enable/disable the submenu's contents (per Apple's HIG).
+ EncodingMenuController encoding_controller;
+ if (encoding_controller.DoesCommandBelongToEncodingMenu(tag)) {
+ enable &= browser_->command_updater()->IsCommandEnabled(
+ IDC_ENCODING_MENU) ? YES : NO;
+ }
+ }
+
+ // If the item is toggleable, find its toggle state and
+ // try to update it. This is a little awkward, but the alternative is
+ // to check after a commandDispatch, which seems worse.
+ [self updateToggleStateWithTag:tag forItem:item];
+ }
+ }
+ return enable;
+}
+
+// Called when the user picks a menu or toolbar item when this window is key.
+// Calls through to the browser object to execute the command. This assumes that
+// the command is supported and doesn't check, otherwise it would have been
+// disabled in the UI in validateUserInterfaceItem:.
+- (void)commandDispatch:(id)sender {
+ DCHECK(sender);
+ // Identify the actual BWC to which the command should be dispatched. It might
+ // belong to a background window, yet this controller gets it because it is
+ // the foreground window's controller and thus in the responder chain. Some
+ // senders don't have this problem (for example, menus only operate on the
+ // foreground window), so this is only an issue for senders that are part of
+ // windows.
+ BrowserWindowController* targetController = self;
+ if ([sender respondsToSelector:@selector(window)])
+ targetController = [[sender window] windowController];
+ DCHECK([targetController isKindOfClass:[BrowserWindowController class]]);
+ DCHECK(targetController->browser_.get());
+ targetController->browser_->ExecuteCommand([sender tag]);
+}
+
+// Same as |-commandDispatch:|, but executes commands using a disposition
+// determined by the key flags. If the window is in the background and the
+// command key is down, ignore the command key, but process any other modifiers.
+- (void)commandDispatchUsingKeyModifiers:(id)sender {
+ DCHECK(sender);
+ // See comment above for why we do this.
+ BrowserWindowController* targetController = self;
+ if ([sender respondsToSelector:@selector(window)])
+ targetController = [[sender window] windowController];
+ DCHECK([targetController isKindOfClass:[BrowserWindowController class]]);
+ NSInteger command = [sender tag];
+ NSUInteger modifierFlags = [[NSApp currentEvent] modifierFlags];
+ if ((command == IDC_RELOAD) &&
+ (modifierFlags & (NSShiftKeyMask | NSControlKeyMask))) {
+ command = IDC_RELOAD_IGNORING_CACHE;
+ // Mask off Shift and Control so they don't affect the disposition below.
+ modifierFlags &= ~(NSShiftKeyMask | NSControlKeyMask);
+ }
+ if (![[sender window] isMainWindow]) {
+ // Remove the command key from the flags, it means "keep the window in
+ // the background" in this case.
+ modifierFlags &= ~NSCommandKeyMask;
+ }
+ WindowOpenDisposition disposition =
+ event_utils::WindowOpenDispositionFromNSEventWithFlags(
+ [NSApp currentEvent], modifierFlags);
+ switch (command) {
+ case IDC_BACK:
+ case IDC_FORWARD:
+ case IDC_RELOAD:
+ case IDC_RELOAD_IGNORING_CACHE:
+ if (disposition == CURRENT_TAB) {
+ // Forcibly reset the location bar, since otherwise it won't discard any
+ // ongoing user edits, since it doesn't realize this is a user-initiated
+ // action.
+ [targetController locationBarBridge]->Revert();
+ }
+ }
+ DCHECK(targetController->browser_.get());
+ targetController->browser_->ExecuteCommandWithDisposition(command,
+ disposition);
+}
+
+// Called when another part of the internal codebase needs to execute a
+// command.
+- (void)executeCommand:(int)command {
+ if (browser_->command_updater()->IsCommandEnabled(command))
+ browser_->ExecuteCommand(command);
+}
+
+// StatusBubble delegate method: tell the status bubble the frame it should
+// position itself in.
+- (NSRect)statusBubbleBaseFrame {
+ NSView* view = [previewableContentsController_ view];
+ return [view convertRect:[view bounds] toView:nil];
+}
+
+- (GTMWindowSheetController*)sheetController {
+ return [tabStripController_ sheetController];
+}
+
+- (void)updateToolbarWithContents:(TabContents*)tab
+ shouldRestoreState:(BOOL)shouldRestore {
+ [toolbarController_ updateToolbarWithContents:tab
+ shouldRestoreState:shouldRestore];
+}
+
+- (void)setStarredState:(BOOL)isStarred {
+ [toolbarController_ setStarredState:isStarred];
+}
+
+// Accept tabs from a BrowserWindowController with the same Profile.
+- (BOOL)canReceiveFrom:(TabWindowController*)source {
+ if (![source isKindOfClass:[BrowserWindowController class]]) {
+ return NO;
+ }
+
+ BrowserWindowController* realSource =
+ static_cast<BrowserWindowController*>(source);
+ if (browser_->profile() != realSource->browser_->profile()) {
+ return NO;
+ }
+
+ // Can't drag a tab from a normal browser to a pop-up
+ if (browser_->type() != realSource->browser_->type()) {
+ return NO;
+ }
+
+ return YES;
+}
+
+// Move a given tab view to the location of the current placeholder. If there is
+// no placeholder, it will go at the end. |controller| is the window controller
+// of a tab being dropped from a different window. It will be nil if the drag is
+// within the window, otherwise the tab is removed from that window before being
+// placed into this one. The implementation will call |-removePlaceholder| since
+// the drag is now complete. This also calls |-layoutTabs| internally so
+// clients do not need to call it again.
+- (void)moveTabView:(NSView*)view
+ fromController:(TabWindowController*)dragController {
+ if (dragController) {
+ // Moving between windows. Figure out the TabContents to drop into our tab
+ // model from the source window's model.
+ BOOL isBrowser =
+ [dragController isKindOfClass:[BrowserWindowController class]];
+ DCHECK(isBrowser);
+ if (!isBrowser) return;
+ BrowserWindowController* dragBWC = (BrowserWindowController*)dragController;
+ int index = [dragBWC->tabStripController_ modelIndexForTabView:view];
+ TabContentsWrapper* contents =
+ dragBWC->browser_->GetTabContentsWrapperAt(index);
+ // The tab contents may have gone away if given a window.close() while it
+ // is being dragged. If so, bail, we've got nothing to drop.
+ if (!contents)
+ return;
+
+ // Convert |view|'s frame (which starts in the source tab strip's coordinate
+ // system) to the coordinate system of the destination tab strip. This needs
+ // to be done before being detached so the window transforms can be
+ // performed.
+ NSRect destinationFrame = [view frame];
+ NSPoint tabOrigin = destinationFrame.origin;
+ tabOrigin = [[dragController tabStripView] convertPoint:tabOrigin
+ toView:nil];
+ tabOrigin = [[view window] convertBaseToScreen:tabOrigin];
+ tabOrigin = [[self window] convertScreenToBase:tabOrigin];
+ tabOrigin = [[self tabStripView] convertPoint:tabOrigin fromView:nil];
+ destinationFrame.origin = tabOrigin;
+
+ // Before the tab is detached from its originating tab strip, store the
+ // pinned state so that it can be maintained between the windows.
+ bool isPinned = dragBWC->browser_->tabstrip_model()->IsTabPinned(index);
+
+ // Now that we have enough information about the tab, we can remove it from
+ // the dragging window. We need to do this *before* we add it to the new
+ // window as this will remove the TabContents' delegate.
+ [dragController detachTabView:view];
+
+ // Deposit it into our model at the appropriate location (it already knows
+ // where it should go from tracking the drag). Doing this sets the tab's
+ // delegate to be the Browser.
+ [tabStripController_ dropTabContents:contents
+ withFrame:destinationFrame
+ asPinnedTab:isPinned];
+ } else {
+ // Moving within a window.
+ int index = [tabStripController_ modelIndexForTabView:view];
+ [tabStripController_ moveTabFromIndex:index];
+ }
+
+ // Remove the placeholder since the drag is now complete.
+ [self removePlaceholder];
+}
+
+// Tells the tab strip to forget about this tab in preparation for it being
+// put into a different tab strip, such as during a drop on another window.
+- (void)detachTabView:(NSView*)view {
+ int index = [tabStripController_ modelIndexForTabView:view];
+ browser_->tabstrip_model()->DetachTabContentsAt(index);
+}
+
+- (NSView*)selectedTabView {
+ return [tabStripController_ selectedTabView];
+}
+
+- (void)setIsLoading:(BOOL)isLoading force:(BOOL)force {
+ [toolbarController_ setIsLoading:isLoading force:force];
+}
+
+// Make the location bar the first responder, if possible.
+- (void)focusLocationBar:(BOOL)selectAll {
+ [toolbarController_ focusLocationBar:selectAll];
+}
+
+- (void)focusTabContents {
+ [[self window] makeFirstResponder:[tabStripController_ selectedTabView]];
+}
+
+- (void)layoutTabs {
+ [tabStripController_ layoutTabs];
+}
+
+- (TabWindowController*)detachTabToNewWindow:(TabView*)tabView {
+ // Disable screen updates so that this appears as a single visual change.
+ app::mac::ScopedNSDisableScreenUpdates disabler;
+
+ // Fetch the tab contents for the tab being dragged.
+ int index = [tabStripController_ modelIndexForTabView:tabView];
+ TabContentsWrapper* contents = browser_->GetTabContentsWrapperAt(index);
+
+ // Set the window size. Need to do this before we detach the tab so it's
+ // still in the window. We have to flip the coordinates as that's what
+ // is expected by the Browser code.
+ NSWindow* sourceWindow = [tabView window];
+ NSRect windowRect = [sourceWindow frame];
+ NSScreen* screen = [sourceWindow screen];
+ windowRect.origin.y =
+ [screen frame].size.height - windowRect.size.height -
+ windowRect.origin.y;
+ gfx::Rect browserRect(windowRect.origin.x, windowRect.origin.y,
+ windowRect.size.width, windowRect.size.height);
+
+ NSRect sourceTabRect = [tabView frame];
+ NSView* tabStrip = [self tabStripView];
+
+ // Pushes tabView's frame back inside the tabstrip.
+ NSSize tabOverflow =
+ [self overflowFrom:[tabStrip convertRectToBase:sourceTabRect]
+ to:[tabStrip frame]];
+ NSRect tabRect = NSOffsetRect(sourceTabRect,
+ -tabOverflow.width, -tabOverflow.height);
+
+ // Before detaching the tab, store the pinned state.
+ bool isPinned = browser_->tabstrip_model()->IsTabPinned(index);
+
+ // Detach it from the source window, which just updates the model without
+ // deleting the tab contents. This needs to come before creating the new
+ // Browser because it clears the TabContents' delegate, which gets hooked
+ // up during creation of the new window.
+ browser_->tabstrip_model()->DetachTabContentsAt(index);
+
+ // Create the new window with a single tab in its model, the one being
+ // dragged.
+ DockInfo dockInfo;
+ Browser* newBrowser = browser_->tabstrip_model()->delegate()->
+ CreateNewStripWithContents(contents, browserRect, dockInfo, false);
+
+ // Propagate the tab pinned state of the new tab (which is the only tab in
+ // this new window).
+ newBrowser->tabstrip_model()->SetTabPinned(0, isPinned);
+
+ // Get the new controller by asking the new window for its delegate.
+ BrowserWindowController* controller =
+ reinterpret_cast<BrowserWindowController*>(
+ [newBrowser->window()->GetNativeHandle() delegate]);
+ DCHECK(controller && [controller isKindOfClass:[TabWindowController class]]);
+
+ // Force the added tab to the right size (remove stretching.)
+ tabRect.size.height = [TabStripController defaultTabHeight];
+
+ // And make sure we use the correct frame in the new view.
+ [[controller tabStripController] setFrameOfSelectedTab:tabRect];
+ return controller;
+}
+
+- (void)insertPlaceholderForTab:(TabView*)tab
+ frame:(NSRect)frame
+ yStretchiness:(CGFloat)yStretchiness {
+ [super insertPlaceholderForTab:tab frame:frame yStretchiness:yStretchiness];
+ [tabStripController_ insertPlaceholderForTab:tab
+ frame:frame
+ yStretchiness:yStretchiness];
+}
+
+- (void)removePlaceholder {
+ [super removePlaceholder];
+ [tabStripController_ insertPlaceholderForTab:nil
+ frame:NSZeroRect
+ yStretchiness:0];
+}
+
+- (BOOL)tabDraggingAllowed {
+ return [tabStripController_ tabDraggingAllowed];
+}
+
+- (BOOL)tabTearingAllowed {
+ return ![self isFullscreen];
+}
+
+- (BOOL)windowMovementAllowed {
+ return ![self isFullscreen];
+}
+
+- (BOOL)isTabFullyVisible:(TabView*)tab {
+ return [tabStripController_ isTabFullyVisible:tab];
+}
+
+- (void)showNewTabButton:(BOOL)show {
+ [tabStripController_ showNewTabButton:show];
+}
+
+- (BOOL)isBookmarkBarVisible {
+ return [bookmarkBarController_ isVisible];
+}
+
+- (BOOL)isBookmarkBarAnimating {
+ return [bookmarkBarController_ isAnimationRunning];
+}
+
+- (void)updateBookmarkBarVisibilityWithAnimation:(BOOL)animate {
+ [bookmarkBarController_
+ updateAndShowNormalBar:[self shouldShowBookmarkBar]
+ showDetachedBar:[self shouldShowDetachedBookmarkBar]
+ withAnimation:animate];
+}
+
+- (BOOL)isDownloadShelfVisible {
+ return downloadShelfController_ != nil &&
+ [downloadShelfController_ isVisible];
+}
+
+- (DownloadShelfController*)downloadShelf {
+ if (!downloadShelfController_.get()) {
+ downloadShelfController_.reset([[DownloadShelfController alloc]
+ initWithBrowser:browser_.get() resizeDelegate:self]);
+ [[[self window] contentView] addSubview:[downloadShelfController_ view]];
+ [downloadShelfController_ show:nil];
+ }
+ return downloadShelfController_;
+}
+
+- (void)addFindBar:(FindBarCocoaController*)findBarCocoaController {
+ // Shouldn't call addFindBar twice.
+ DCHECK(!findBarCocoaController_.get());
+
+ // Create a controller for the findbar.
+ findBarCocoaController_.reset([findBarCocoaController retain]);
+ NSView *contentView = [[self window] contentView];
+ [contentView addSubview:[findBarCocoaController_ view]
+ positioned:NSWindowAbove
+ relativeTo:[toolbarController_ view]];
+
+ // Place the find bar immediately below the toolbar/attached bookmark bar. In
+ // fullscreen mode, it hangs off the top of the screen when the bar is hidden.
+ CGFloat maxY = [self placeBookmarkBarBelowInfoBar] ?
+ NSMinY([[toolbarController_ view] frame]) :
+ NSMinY([[bookmarkBarController_ view] frame]);
+ CGFloat maxWidth = NSWidth([contentView frame]);
+ [findBarCocoaController_ positionFindBarViewAtMaxY:maxY maxWidth:maxWidth];
+}
+
+- (NSWindow*)createFullscreenWindow {
+ return [[[FullscreenWindow alloc] initForScreen:[[self window] screen]]
+ autorelease];
+}
+
+- (NSInteger)numberOfTabs {
+ // count() includes pinned tabs.
+ return browser_->tabstrip_model()->count();
+}
+
+- (BOOL)hasLiveTabs {
+ return !browser_->tabstrip_model()->empty();
+}
+
+- (NSString*)selectedTabTitle {
+ TabContents* contents = browser_->GetSelectedTabContents();
+ return base::SysUTF16ToNSString(contents->GetTitle());
+}
+
+- (NSRect)regularWindowFrame {
+ return [self isFullscreen] ? [savedRegularWindow_ frame] :
+ [[self window] frame];
+}
+
+// (Override of |TabWindowController| method.)
+- (BOOL)hasTabStrip {
+ return [self supportsWindowFeature:Browser::FEATURE_TABSTRIP];
+}
+
+// TabContentsControllerDelegate protocol.
+- (void)tabContentsViewFrameWillChange:(TabContentsController*)source
+ frameRect:(NSRect)frameRect {
+ TabContents* contents = [source tabContents];
+ RenderWidgetHostView* render_widget_host_view = contents ?
+ contents->GetRenderWidgetHostView() : NULL;
+ if (!render_widget_host_view)
+ return;
+
+ gfx::Rect reserved_rect;
+
+ NSWindow* window = [self window];
+ if ([window respondsToSelector:@selector(_growBoxRect)]) {
+ NSView* view = [source view];
+ if (view && [view superview]) {
+ NSRect windowGrowBoxRect = [window _growBoxRect];
+ NSRect viewRect = [[view superview] convertRect:frameRect toView:nil];
+ NSRect growBoxRect = NSIntersectionRect(windowGrowBoxRect, viewRect);
+ if (!NSIsEmptyRect(growBoxRect)) {
+ // Before we return a rect, we need to convert it from window
+ // coordinates to content area coordinates and flip the coordinate
+ // system.
+ // Superview is used here because, first, it's a frame rect, so it is
+ // specified in the parent's coordinates and, second, view is not
+ // positioned yet.
+ growBoxRect = [[view superview] convertRect:growBoxRect fromView:nil];
+ growBoxRect.origin.y =
+ NSHeight(frameRect) - NSHeight(growBoxRect);
+ growBoxRect =
+ NSOffsetRect(growBoxRect, -frameRect.origin.x, -frameRect.origin.y);
+
+ reserved_rect =
+ gfx::Rect(growBoxRect.origin.x, growBoxRect.origin.y,
+ growBoxRect.size.width, growBoxRect.size.height);
+ }
+ }
+ }
+
+ render_widget_host_view->set_reserved_contents_rect(reserved_rect);
+}
+
+// TabStripControllerDelegate protocol.
+- (void)onSelectTabWithContents:(TabContents*)contents {
+ // Update various elements that are interested in knowing the current
+ // TabContents.
+
+ // Update all the UI bits.
+ windowShim_->UpdateTitleBar();
+
+ [sidebarController_ updateSidebarForTabContents:contents];
+ [devToolsController_ updateDevToolsForTabContents:contents];
+
+ // Update the bookmark bar.
+ // Must do it after sidebar and devtools update, otherwise bookmark bar might
+ // call resizeView -> layoutSubviews and cause unnecessary relayout.
+ // TODO(viettrungluu): perhaps update to not terminate running animations (if
+ // applicable)?
+ [self updateBookmarkBarVisibilityWithAnimation:NO];
+
+ [infoBarContainerController_ changeTabContents:contents];
+
+ // Update devTools and sidebar contents after size for all views is set.
+ [sidebarController_ ensureContentsVisible];
+ [devToolsController_ ensureContentsVisible];
+}
+
+- (void)onReplaceTabWithContents:(TabContents*)contents {
+ // This is only called when instant results are committed. Simply remove the
+ // preview view; the tab strip controller will reinstall the view as the
+ // active view.
+ [previewableContentsController_ hidePreview];
+ [self updateBookmarkBarVisibilityWithAnimation:NO];
+}
+
+- (void)onSelectedTabChange:(TabStripModelObserver::TabChangeType)change {
+ // Update titles if this is the currently selected tab and if it isn't just
+ // the loading state which changed.
+ if (change != TabStripModelObserver::LOADING_ONLY)
+ windowShim_->UpdateTitleBar();
+
+ // Update the bookmark bar if this is the currently selected tab and if it
+ // isn't just the title which changed. This for transitions between the NTP
+ // (showing its floating bookmark bar) and normal web pages (showing no
+ // bookmark bar).
+ // TODO(viettrungluu): perhaps update to not terminate running animations?
+ if (change != TabStripModelObserver::TITLE_NOT_LOADING)
+ [self updateBookmarkBarVisibilityWithAnimation:NO];
+}
+
+- (void)onTabDetachedWithContents:(TabContents*)contents {
+ [infoBarContainerController_ tabDetachedWithContents:contents];
+}
+
+- (void)userChangedTheme {
+ // TODO(dmaclach): Instead of redrawing the whole window, views that care
+ // about the active window state should be registering for notifications.
+ [[self window] setViewsNeedDisplay:YES];
+}
+
+- (ThemeProvider*)themeProvider {
+ return browser_->profile()->GetThemeProvider();
+}
+
+- (ThemedWindowStyle)themedWindowStyle {
+ ThemedWindowStyle style = 0;
+ if (browser_->profile()->IsOffTheRecord())
+ style |= THEMED_INCOGNITO;
+
+ Browser::Type type = browser_->type();
+ if (type == Browser::TYPE_POPUP)
+ style |= THEMED_POPUP;
+ else if (type == Browser::TYPE_DEVTOOLS)
+ style |= THEMED_DEVTOOLS;
+
+ return style;
+}
+
+- (NSPoint)themePatternPhase {
+ // Our patterns want to be drawn from the upper left hand corner of the view.
+ // Cocoa wants to do it from the lower left of the window.
+ //
+ // Rephase our pattern to fit this view. Some other views (Tabs, Toolbar etc.)
+ // will phase their patterns relative to this so all the views look right.
+ //
+ // To line up the background pattern with the pattern in the browser window
+ // the background pattern for the tabs needs to be moved left by 5 pixels.
+ const CGFloat kPatternHorizontalOffset = -5;
+ NSView* tabStripView = [self tabStripView];
+ NSRect tabStripViewWindowBounds = [tabStripView bounds];
+ NSView* windowChromeView = [[[self window] contentView] superview];
+ tabStripViewWindowBounds =
+ [tabStripView convertRect:tabStripViewWindowBounds
+ toView:windowChromeView];
+ NSPoint phase = NSMakePoint(NSMinX(tabStripViewWindowBounds)
+ + kPatternHorizontalOffset,
+ NSMinY(tabStripViewWindowBounds)
+ + [TabStripController defaultTabHeight]);
+ return phase;
+}
+
+- (NSPoint)bookmarkBubblePoint {
+ return [toolbarController_ bookmarkBubblePoint];
+}
+
+// Show the bookmark bubble (e.g. user just clicked on the STAR).
+- (void)showBookmarkBubbleForURL:(const GURL&)url
+ alreadyBookmarked:(BOOL)alreadyMarked {
+ if (!bookmarkBubbleController_) {
+ BookmarkModel* model = browser_->profile()->GetBookmarkModel();
+ const BookmarkNode* node = model->GetMostRecentlyAddedNodeForURL(url);
+ bookmarkBubbleController_ =
+ [[BookmarkBubbleController alloc] initWithParentWindow:[self window]
+ model:model
+ node:node
+ alreadyBookmarked:alreadyMarked];
+ [bookmarkBubbleController_ showWindow:self];
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
+ selector:@selector(bubbleWindowWillClose:)
+ name:NSWindowWillCloseNotification
+ object:[bookmarkBubbleController_ window]];
+ }
+}
+
+// Nil out the weak bookmark bubble controller reference.
+- (void)bubbleWindowWillClose:(NSNotification*)notification {
+ DCHECK([notification object] == [bookmarkBubbleController_ window]);
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center removeObserver:self
+ name:NSWindowWillCloseNotification
+ object:[bookmarkBubbleController_ window]];
+ bookmarkBubbleController_ = nil;
+}
+
+// Handle the editBookmarkNode: action sent from bookmark bubble controllers.
+- (void)editBookmarkNode:(id)sender {
+ BOOL responds = [sender respondsToSelector:@selector(node)];
+ DCHECK(responds);
+ if (responds) {
+ const BookmarkNode* node = [sender node];
+ if (node) {
+ // A BookmarkEditorController is a sheet that owns itself, and
+ // deallocates itself when closed.
+ [[[BookmarkEditorController alloc]
+ initWithParentWindow:[self window]
+ profile:browser_->profile()
+ parent:node->GetParent()
+ node:node
+ configuration:BookmarkEditor::SHOW_TREE]
+ runAsModalSheet];
+ }
+ }
+}
+
+// If the browser is in incognito mode, install the image view to decorate
+// the window at the upper right. Use the same base y coordinate as the
+// tab strip.
+- (void)installIncognitoBadge {
+ // Only install if this browser window is OTR and has a tab strip.
+ if (!browser_->profile()->IsOffTheRecord() || ![self hasTabStrip])
+ return;
+
+ // Install the image into the badge view and size the view appropriately.
+ // Hide it for now; positioning and showing will be done by the layout code.
+ NSImage* image = nsimage_cache::ImageNamed(@"otr_icon.pdf");
+ incognitoBadge_.reset([[IncognitoImageView alloc] init]);
+ [incognitoBadge_ setImage:image];
+ [incognitoBadge_ setFrameSize:[image size]];
+ [incognitoBadge_ setAutoresizingMask:NSViewMinXMargin | NSViewMinYMargin];
+ [incognitoBadge_ setHidden:YES];
+
+ // Give it a shadow.
+ scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
+ [shadow.get() setShadowColor:[NSColor colorWithCalibratedWhite:0.0
+ alpha:0.5]];
+ [shadow.get() setShadowOffset:NSMakeSize(0, -1)];
+ [shadow setShadowBlurRadius:2.0];
+ [incognitoBadge_ setShadow:shadow];
+
+ // Install the view.
+ [[[[self window] contentView] superview] addSubview:incognitoBadge_];
+}
+
+// Documented in 10.6+, but present starting in 10.5. Called when we get a
+// three-finger swipe.
+- (void)swipeWithEvent:(NSEvent*)event {
+ // Map forwards and backwards to history; left is positive, right is negative.
+ unsigned int command = 0;
+ if ([event deltaX] > 0.5) {
+ command = IDC_BACK;
+ } else if ([event deltaX] < -0.5) {
+ command = IDC_FORWARD;
+ } else if ([event deltaY] > 0.5) {
+ // TODO(pinkerton): figure out page-up, http://crbug.com/16305
+ } else if ([event deltaY] < -0.5) {
+ // TODO(pinkerton): figure out page-down, http://crbug.com/16305
+ browser_->ExecuteCommand(IDC_TABPOSE);
+ }
+
+ // Ensure the command is valid first (ExecuteCommand() won't do that) and
+ // then make it so.
+ if (browser_->command_updater()->IsCommandEnabled(command))
+ browser_->ExecuteCommandWithDisposition(command,
+ event_utils::WindowOpenDispositionFromNSEvent(event));
+}
+
+// Documented in 10.6+, but present starting in 10.5. Called repeatedly during
+// a pinch gesture, with incremental change values.
+- (void)magnifyWithEvent:(NSEvent*)event {
+ // The deltaZ difference necessary to trigger a zoom action. Derived from
+ // experimentation to find a value that feels reasonable.
+ const float kZoomStepValue = 150;
+
+ // Find the (absolute) thresholds on either side of the current zoom factor,
+ // then convert those to actual numbers to trigger a zoom in or out.
+ // This logic deliberately makes the range around the starting zoom value for
+ // the gesture twice as large as the other ranges (i.e., the notches are at
+ // ..., -3*step, -2*step, -step, step, 2*step, 3*step, ... but not at 0)
+ // so that it's easier to get back to your starting point than it is to
+ // overshoot.
+ float nextStep = (abs(currentZoomStepDelta_) + 1) * kZoomStepValue;
+ float backStep = abs(currentZoomStepDelta_) * kZoomStepValue;
+ float zoomInThreshold = (currentZoomStepDelta_ >= 0) ? nextStep : -backStep;
+ float zoomOutThreshold = (currentZoomStepDelta_ <= 0) ? -nextStep : backStep;
+
+ unsigned int command = 0;
+ totalMagnifyGestureAmount_ += [event deltaZ];
+ if (totalMagnifyGestureAmount_ > zoomInThreshold) {
+ command = IDC_ZOOM_PLUS;
+ } else if (totalMagnifyGestureAmount_ < zoomOutThreshold) {
+ command = IDC_ZOOM_MINUS;
+ }
+
+ if (command && browser_->command_updater()->IsCommandEnabled(command)) {
+ currentZoomStepDelta_ += (command == IDC_ZOOM_PLUS) ? 1 : -1;
+ browser_->ExecuteCommandWithDisposition(command,
+ event_utils::WindowOpenDispositionFromNSEvent(event));
+ }
+}
+
+// Documented in 10.6+, but present starting in 10.5. Called at the beginning
+// of a gesture.
+- (void)beginGestureWithEvent:(NSEvent*)event {
+ totalMagnifyGestureAmount_ = 0;
+ currentZoomStepDelta_ = 0;
+}
+
+// Delegate method called when window is resized.
+- (void)windowDidResize:(NSNotification*)notification {
+ // Resize (and possibly move) the status bubble. Note that we may get called
+ // when the status bubble does not exist.
+ if (statusBubble_) {
+ statusBubble_->UpdateSizeAndPosition();
+ }
+
+ // Let the selected RenderWidgetHostView know, so that it can tell plugins.
+ if (TabContents* contents = browser_->GetSelectedTabContents()) {
+ if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView())
+ rwhv->WindowFrameChanged();
+ }
+}
+
+// Handle the openLearnMoreAboutCrashLink: action from SadTabController when
+// "Learn more" link in "Aw snap" page (i.e. crash page or sad tab) is
+// clicked. Decoupling the action from its target makes unitestting possible.
+- (void)openLearnMoreAboutCrashLink:(id)sender {
+ if ([sender isKindOfClass:[SadTabController class]]) {
+ SadTabController* sad_tab = static_cast<SadTabController*>(sender);
+ TabContents* tab_contents = [sad_tab tabContents];
+ if (tab_contents) {
+ GURL helpUrl =
+ google_util::AppendGoogleLocaleParam(GURL(chrome::kCrashReasonURL));
+ tab_contents->OpenURL(helpUrl, GURL(), CURRENT_TAB, PageTransition::LINK);
+ }
+ }
+}
+
+// Delegate method called when window did move. (See below for why we don't use
+// |-windowWillMove:|, which is called less frequently than |-windowDidMove|
+// instead.)
+- (void)windowDidMove:(NSNotification*)notification {
+ NSWindow* window = [self window];
+ NSRect windowFrame = [window frame];
+ NSRect workarea = [[window screen] visibleFrame];
+
+ // We reset the window growth state whenever the window is moved out of the
+ // work area or away (up or down) from the bottom or top of the work area.
+ // Unfortunately, Cocoa sends |-windowWillMove:| too frequently (including
+ // when clicking on the title bar to activate), and of course
+ // |-windowWillMove| is called too early for us to apply our heuristic. (The
+ // heuristic we use for detecting window movement is that if |windowTopGrowth_
+ // > 0|, then we should be at the bottom of the work area -- if we're not,
+ // we've moved. Similarly for the other side.)
+ if (!NSContainsRect(workarea, windowFrame) ||
+ (windowTopGrowth_ > 0 && NSMinY(windowFrame) != NSMinY(workarea)) ||
+ (windowBottomGrowth_ > 0 && NSMaxY(windowFrame) != NSMaxY(workarea)))
+ [self resetWindowGrowthState];
+
+ // Let the selected RenderWidgetHostView know, so that it can tell plugins.
+ if (TabContents* contents = browser_->GetSelectedTabContents()) {
+ if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView())
+ rwhv->WindowFrameChanged();
+ }
+}
+
+// Delegate method called when window will be resized; not called for
+// |-setFrame:display:|.
+- (NSSize)windowWillResize:(NSWindow*)sender toSize:(NSSize)frameSize {
+ [self resetWindowGrowthState];
+ return frameSize;
+}
+
+// Delegate method: see |NSWindowDelegate| protocol.
+- (id)windowWillReturnFieldEditor:(NSWindow*)sender toObject:(id)obj {
+ // Ask the toolbar controller if it wants to return a custom field editor
+ // for the specific object.
+ return [toolbarController_ customFieldEditorForObject:obj];
+}
+
+// (Needed for |BookmarkBarControllerDelegate| protocol.)
+- (void)bookmarkBar:(BookmarkBarController*)controller
+ didChangeFromState:(bookmarks::VisualState)oldState
+ toState:(bookmarks::VisualState)newState {
+ [toolbarController_
+ setDividerOpacity:[bookmarkBarController_ toolbarDividerOpacity]];
+ [self adjustToolbarAndBookmarkBarForCompression:
+ [controller getDesiredToolbarHeightCompression]];
+}
+
+// (Needed for |BookmarkBarControllerDelegate| protocol.)
+- (void)bookmarkBar:(BookmarkBarController*)controller
+willAnimateFromState:(bookmarks::VisualState)oldState
+ toState:(bookmarks::VisualState)newState {
+ [toolbarController_
+ setDividerOpacity:[bookmarkBarController_ toolbarDividerOpacity]];
+ [self adjustToolbarAndBookmarkBarForCompression:
+ [controller getDesiredToolbarHeightCompression]];
+}
+
+// (Private/TestingAPI)
+- (void)resetWindowGrowthState {
+ windowTopGrowth_ = 0;
+ windowBottomGrowth_ = 0;
+ isShrinkingFromZoomed_ = NO;
+}
+
+- (NSSize)overflowFrom:(NSRect)source
+ to:(NSRect)target {
+ // If |source|'s boundary is outside of |target|'s, set its distance
+ // to |x|. Note that |source| can overflow to both side, but we
+ // have nothing to do for such case.
+ CGFloat x = 0;
+ if (NSMaxX(target) < NSMaxX(source)) // |source| overflows to right
+ x = NSMaxX(source) - NSMaxX(target);
+ else if (NSMinX(source) < NSMinX(target)) // |source| overflows to left
+ x = NSMinX(source) - NSMinX(target);
+
+ // Same as |x| above.
+ CGFloat y = 0;
+ if (NSMaxY(target) < NSMaxY(source))
+ y = NSMaxY(source) - NSMaxY(target);
+ else if (NSMinY(source) < NSMinY(target))
+ y = NSMinY(source) - NSMinY(target);
+
+ return NSMakeSize(x, y);
+}
+
+// Override to swap in the correct tab strip controller based on the new
+// tab strip mode.
+- (void)toggleTabStripDisplayMode {
+ [super toggleTabStripDisplayMode];
+ [self createTabStripController];
+}
+
+- (BOOL)useVerticalTabs {
+ return browser_->tabstrip_model()->delegate()->UseVerticalTabs();
+}
+
+- (void)showInstant:(TabContents*)previewContents {
+ [previewableContentsController_ showPreview:previewContents];
+ [self updateBookmarkBarVisibilityWithAnimation:NO];
+}
+
+- (void)hideInstant {
+ // TODO(rohitrao): Revisit whether or not this method should be called when
+ // instant isn't showing.
+ if (![previewableContentsController_ isShowingPreview])
+ return;
+
+ [previewableContentsController_ hidePreview];
+ [self updateBookmarkBarVisibilityWithAnimation:NO];
+}
+
+- (NSRect)instantFrame {
+ // The view's bounds are in its own coordinate system. Convert that to the
+ // window base coordinate system, then translate it into the screen's
+ // coordinate system.
+ NSView* view = [previewableContentsController_ view];
+ if (!view)
+ return NSZeroRect;
+
+ NSRect frame = [view convertRect:[view bounds] toView:nil];
+ NSPoint originInScreenCoords =
+ [[view window] convertBaseToScreen:frame.origin];
+ frame.origin = originInScreenCoords;
+ return frame;
+}
+
+- (void)sheetDidEnd:(NSWindow*)sheet
+ returnCode:(NSInteger)code
+ context:(void*)context {
+ [sheet orderOut:self];
+}
+
+@end // @implementation BrowserWindowController
+
+
+@implementation BrowserWindowController(Fullscreen)
+
+- (void)setFullscreen:(BOOL)fullscreen {
+ // The logic in this function is a bit complicated and very carefully
+ // arranged. See the below comments for more details.
+
+ if (fullscreen == [self isFullscreen])
+ return;
+
+ if (![self supportsFullscreen])
+ return;
+
+ // Fade to black.
+ const CGDisplayReservationInterval kFadeDurationSeconds = 0.6;
+ Boolean didFadeOut = NO;
+ CGDisplayFadeReservationToken token;
+ if (CGAcquireDisplayFadeReservation(kFadeDurationSeconds, &token)
+ == kCGErrorSuccess) {
+ didFadeOut = YES;
+ CGDisplayFade(token, kFadeDurationSeconds / 2, kCGDisplayBlendNormal,
+ kCGDisplayBlendSolidColor, 0.0, 0.0, 0.0, /*synchronous=*/true);
+ }
+
+ // Close the bookmark bubble, if it's open. We use |-ok:| instead of
+ // |-cancel:| or |-close| because that matches the behavior when the bubble
+ // loses key status.
+ [bookmarkBubbleController_ ok:self];
+
+ // Save the current first responder so we can restore after views are moved.
+ NSWindow* window = [self window];
+ scoped_nsobject<FocusTracker> focusTracker(
+ [[FocusTracker alloc] initWithWindow:window]);
+ BOOL showDropdown = [self floatingBarHasFocus];
+
+ // While we move views (and focus) around, disable any bar visibility changes.
+ [self disableBarVisibilityUpdates];
+
+ // If we're entering fullscreen, create the fullscreen controller. If we're
+ // exiting fullscreen, kill the controller.
+ if (fullscreen) {
+ fullscreenController_.reset([[FullscreenController alloc]
+ initWithBrowserController:self]);
+ } else {
+ [fullscreenController_ exitFullscreen];
+ fullscreenController_.reset();
+ }
+
+ // Destroy the tab strip's sheet controller. We will recreate it in the new
+ // window when needed.
+ [tabStripController_ destroySheetController];
+
+ // Retain the tab strip view while we remove it from its superview.
+ scoped_nsobject<NSView> tabStripView;
+ if ([self hasTabStrip] && ![self useVerticalTabs]) {
+ tabStripView.reset([[self tabStripView] retain]);
+ [tabStripView removeFromSuperview];
+ }
+
+ // Ditto for the content view.
+ scoped_nsobject<NSView> contentView([[window contentView] retain]);
+ // Disable autoresizing of subviews while we move views around. This prevents
+ // spurious renderer resizes.
+ [contentView setAutoresizesSubviews:NO];
+ [contentView removeFromSuperview];
+
+ NSWindow* destWindow = nil;
+ if (fullscreen) {
+ DCHECK(!savedRegularWindow_);
+ savedRegularWindow_ = [window retain];
+ destWindow = [self createFullscreenWindow];
+ } else {
+ DCHECK(savedRegularWindow_);
+ destWindow = [savedRegularWindow_ autorelease];
+ savedRegularWindow_ = nil;
+
+ CGSWorkspaceID workspace;
+ if ([window cr_workspace:&workspace]) {
+ [destWindow cr_moveToWorkspace:workspace];
+ }
+ }
+ DCHECK(destWindow);
+
+ // Have to do this here, otherwise later calls can crash because the window
+ // has no delegate.
+ [window setDelegate:nil];
+ [destWindow setDelegate:self];
+
+ // With this call, valgrind complains that a "Conditional jump or move depends
+ // on uninitialised value(s)". The error happens in -[NSThemeFrame
+ // drawOverlayRect:]. I'm pretty convinced this is an Apple bug, but there is
+ // no visual impact. I have been unable to tickle it away with other window
+ // or view manipulation Cocoa calls. Stack added to suppressions_mac.txt.
+ [contentView setAutoresizesSubviews:YES];
+ [destWindow setContentView:contentView];
+
+ // Move the incognito badge if present.
+ if (incognitoBadge_.get()) {
+ [incognitoBadge_ removeFromSuperview];
+ [incognitoBadge_ setHidden:YES]; // Will be shown in layout.
+ [[[destWindow contentView] superview] addSubview:incognitoBadge_];
+ }
+
+ // Add the tab strip after setting the content view and moving the incognito
+ // badge (if any), so that the tab strip will be on top (in the z-order).
+ if ([self hasTabStrip] && ![self useVerticalTabs])
+ [[[destWindow contentView] superview] addSubview:tabStripView];
+
+ [window setWindowController:nil];
+ [self setWindow:destWindow];
+ [destWindow setWindowController:self];
+ [self adjustUIForFullscreen:fullscreen];
+
+ // When entering fullscreen mode, the controller forces a layout for us. When
+ // exiting, we need to call layoutSubviews manually.
+ if (fullscreen) {
+ [fullscreenController_ enterFullscreenForContentView:contentView
+ showDropdown:showDropdown];
+ } else {
+ [self layoutSubviews];
+ }
+
+ // Move the status bubble over, if we have one.
+ if (statusBubble_)
+ statusBubble_->SwitchParentWindow(destWindow);
+
+ // Move the title over.
+ [destWindow setTitle:[window title]];
+
+ // The window needs to be onscreen before we can set its first responder.
+ [destWindow makeKeyAndOrderFront:self];
+ [focusTracker restoreFocusInWindow:destWindow];
+ [window orderOut:self];
+
+ // We're done moving focus, so re-enable bar visibility changes.
+ [self enableBarVisibilityUpdates];
+
+ // Fade back in.
+ if (didFadeOut) {
+ CGDisplayFade(token, kFadeDurationSeconds / 2, kCGDisplayBlendSolidColor,
+ kCGDisplayBlendNormal, 0.0, 0.0, 0.0, /*synchronous=*/false);
+ CGReleaseDisplayFadeReservation(token);
+ }
+}
+
+- (BOOL)isFullscreen {
+ return fullscreenController_.get() && [fullscreenController_ isFullscreen];
+}
+
+- (void)resizeFullscreenWindow {
+ DCHECK([self isFullscreen]);
+ if (![self isFullscreen])
+ return;
+
+ NSWindow* window = [self window];
+ [window setFrame:[[window screen] frame] display:YES];
+ [self layoutSubviews];
+}
+
+- (CGFloat)floatingBarShownFraction {
+ return floatingBarShownFraction_;
+}
+
+- (void)setFloatingBarShownFraction:(CGFloat)fraction {
+ floatingBarShownFraction_ = fraction;
+ [self layoutSubviews];
+}
+
+- (BOOL)isBarVisibilityLockedForOwner:(id)owner {
+ DCHECK(owner);
+ DCHECK(barVisibilityLocks_);
+ return [barVisibilityLocks_ containsObject:owner];
+}
+
+- (void)lockBarVisibilityForOwner:(id)owner
+ withAnimation:(BOOL)animate
+ delay:(BOOL)delay {
+ if (![self isBarVisibilityLockedForOwner:owner]) {
+ [barVisibilityLocks_ addObject:owner];
+
+ // If enabled, show the overlay if necessary (and if in fullscreen mode).
+ if (barVisibilityUpdatesEnabled_) {
+ [fullscreenController_ ensureOverlayShownWithAnimation:animate
+ delay:delay];
+ }
+ }
+}
+
+- (void)releaseBarVisibilityForOwner:(id)owner
+ withAnimation:(BOOL)animate
+ delay:(BOOL)delay {
+ if ([self isBarVisibilityLockedForOwner:owner]) {
+ [barVisibilityLocks_ removeObject:owner];
+
+ // If enabled, hide the overlay if necessary (and if in fullscreen mode).
+ if (barVisibilityUpdatesEnabled_ &&
+ ![barVisibilityLocks_ count]) {
+ [fullscreenController_ ensureOverlayHiddenWithAnimation:animate
+ delay:delay];
+ }
+ }
+}
+
+- (BOOL)floatingBarHasFocus {
+ NSResponder* focused = [[self window] firstResponder];
+ return [focused isKindOfClass:[AutocompleteTextFieldEditor class]];
+}
+
+- (void)openTabpose {
+ NSUInteger modifierFlags = [[NSApp currentEvent] modifierFlags];
+ BOOL slomo = (modifierFlags & NSShiftKeyMask) != 0;
+
+ // Cover info bars, inspector window, and detached bookmark bar on NTP.
+ // Do not cover download shelf.
+ NSRect activeArea = [[self tabContentArea] frame];
+ activeArea.size.height +=
+ NSHeight([[infoBarContainerController_ view] frame]);
+ if ([self isBookmarkBarVisible] && [self placeBookmarkBarBelowInfoBar]) {
+ NSView* bookmarkBarView = [bookmarkBarController_ view];
+ activeArea.size.height += NSHeight([bookmarkBarView frame]);
+ }
+
+ [TabposeWindow openTabposeFor:[self window]
+ rect:activeArea
+ slomo:slomo
+ tabStripModel:browser_->tabstrip_model()];
+}
+
+@end // @implementation BrowserWindowController(Fullscreen)
+
+
+@implementation BrowserWindowController(WindowType)
+
+- (BOOL)supportsWindowFeature:(int)feature {
+ return browser_->SupportsWindowFeature(
+ static_cast<Browser::WindowFeature>(feature));
+}
+
+- (BOOL)hasTitleBar {
+ return [self supportsWindowFeature:Browser::FEATURE_TITLEBAR];
+}
+
+- (BOOL)hasToolbar {
+ return [self supportsWindowFeature:Browser::FEATURE_TOOLBAR];
+}
+
+- (BOOL)hasLocationBar {
+ return [self supportsWindowFeature:Browser::FEATURE_LOCATIONBAR];
+}
+
+- (BOOL)supportsBookmarkBar {
+ return [self supportsWindowFeature:Browser::FEATURE_BOOKMARKBAR];
+}
+
+- (BOOL)isNormalWindow {
+ return browser_->type() == Browser::TYPE_NORMAL;
+}
+
+@end // @implementation BrowserWindowController(WindowType)
diff --git a/chrome/browser/ui/cocoa/browser_window_controller_private.h b/chrome/browser/ui/cocoa/browser_window_controller_private.h
new file mode 100644
index 0000000..87571262
--- /dev/null
+++ b/chrome/browser/ui/cocoa/browser_window_controller_private.h
@@ -0,0 +1,119 @@
+// 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_UI_COCOA_BROWSER_WINDOW_CONTROLLER_PRIVATE_H_
+#define CHROME_BROWSER_UI_COCOA_BROWSER_WINDOW_CONTROLLER_PRIVATE_H_
+#pragma once
+
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+
+
+// Private methods for the |BrowserWindowController|. This category should
+// contain the private methods used by different parts of the BWC; private
+// methods used only by single parts should be declared in their own file.
+// TODO(viettrungluu): [crbug.com/35543] work on splitting out stuff from the
+// BWC, and figuring out which methods belong here (need to unravel
+// "dependencies").
+@interface BrowserWindowController(Private)
+
+// Create the appropriate tab strip controller based on whether or not side
+// tabs are enabled. Replaces the current controller.
+- (void)createTabStripController;
+
+// Saves the window's position in the local state preferences.
+- (void)saveWindowPositionIfNeeded;
+
+// Saves the window's position to the given pref service.
+- (void)saveWindowPositionToPrefs:(PrefService*)prefs;
+
+// We need to adjust where sheets come out of the window, as by default they
+// erupt from the omnibox, which is rather weird.
+- (NSRect)window:(NSWindow*)window
+ willPositionSheet:(NSWindow*)sheet
+ usingRect:(NSRect)defaultSheetRect;
+
+// Repositions the window's subviews. From the top down: toolbar, normal
+// bookmark bar (if shown), infobar, NTP detached bookmark bar (if shown),
+// content area, download shelf (if any).
+- (void)layoutSubviews;
+
+// Find the total height of the floating bar (in fullscreen mode). Safe to call
+// even when not in fullscreen mode.
+- (CGFloat)floatingBarHeight;
+
+// Lays out the tab strip at the given maximum y-coordinate, with the given
+// width, possibly for fullscreen mode; returns the new maximum y (below the tab
+// strip). This is safe to call even when there is no tab strip.
+- (CGFloat)layoutTabStripAtMaxY:(CGFloat)maxY
+ width:(CGFloat)width
+ fullscreen:(BOOL)fullscreen;
+
+// Lays out the toolbar (or just location bar for popups) at the given maximum
+// y-coordinate, with the given width; returns the new maximum y (below the
+// toolbar).
+- (CGFloat)layoutToolbarAtMinX:(CGFloat)minX
+ maxY:(CGFloat)maxY
+ width:(CGFloat)width;
+
+// Returns YES if the bookmark bar should be placed below the infobar, NO
+// otherwise.
+- (BOOL)placeBookmarkBarBelowInfoBar;
+
+// Lays out the bookmark bar at the given maximum y-coordinate, with the given
+// width; returns the new maximum y (below the bookmark bar). Note that one must
+// call it with the appropriate |maxY| which depends on whether or not the
+// bookmark bar is shown as the NTP bubble or not (use
+// |-placeBookmarkBarBelowInfoBar|).
+- (CGFloat)layoutBookmarkBarAtMinX:(CGFloat)minX
+ maxY:(CGFloat)maxY
+ width:(CGFloat)width;
+
+// Lay out the view which draws the background for the floating bar when in
+// fullscreen mode, with the given frame and fullscreen-mode-status. Should be
+// called even when not in fullscreen mode to hide the backing view.
+- (void)layoutFloatingBarBackingView:(NSRect)frame
+ fullscreen:(BOOL)fullscreen;
+
+// Lays out the infobar at the given maximum y-coordinate, with the given width;
+// returns the new maximum y (below the infobar).
+- (CGFloat)layoutInfoBarAtMinX:(CGFloat)minX
+ maxY:(CGFloat)maxY
+ width:(CGFloat)width;
+
+// Lays out the download shelf, if there is one, at the given minimum
+// y-coordinate, with the given width; returns the new minimum y (above the
+// download shelf). This is safe to call even if there is no download shelf.
+- (CGFloat)layoutDownloadShelfAtMinX:(CGFloat)minX
+ minY:(CGFloat)minY
+ width:(CGFloat)width;
+
+// Lays out the tab content area in the given frame. If the height changes,
+// sends a message to the renderer to resize.
+- (void)layoutTabContentArea:(NSRect)frame;
+
+// Should we show the normal bookmark bar?
+- (BOOL)shouldShowBookmarkBar;
+
+// Is the current page one for which the bookmark should be shown detached *if*
+// the normal bookmark bar is not shown?
+- (BOOL)shouldShowDetachedBookmarkBar;
+
+// Sets the toolbar's height to a value appropriate for the given compression.
+// Also adjusts the bookmark bar's height by the opposite amount in order to
+// keep the total height of the two views constant.
+- (void)adjustToolbarAndBookmarkBarForCompression:(CGFloat)compression;
+
+// Adjust the UI when entering or leaving fullscreen mode.
+- (void)adjustUIForFullscreen:(BOOL)fullscreen;
+
+// Allows/prevents bar visibility locks and releases from updating the visual
+// state. Enabling makes changes instantaneously; disabling cancels any
+// timers/animation.
+- (void)enableBarVisibilityUpdates;
+- (void)disableBarVisibilityUpdates;
+
+@end // @interface BrowserWindowController(Private)
+
+
+#endif // CHROME_BROWSER_UI_COCOA_BROWSER_WINDOW_CONTROLLER_PRIVATE_H_
diff --git a/chrome/browser/ui/cocoa/browser_window_controller_private.mm b/chrome/browser/ui/cocoa/browser_window_controller_private.mm
new file mode 100644
index 0000000..62d0a4f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/browser_window_controller_private.mm
@@ -0,0 +1,509 @@
+// 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/ui/cocoa/browser_window_controller_private.h"
+
+#include "base/mac_util.h"
+#import "base/scoped_nsobject.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/renderer_host/render_widget_host_view.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents/tab_contents_view.h"
+#include "chrome/browser/themes/browser_theme_provider.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list.h"
+#import "chrome/browser/ui/cocoa/fast_resize_view.h"
+#import "chrome/browser/ui/cocoa/find_bar_cocoa_controller.h"
+#import "chrome/browser/ui/cocoa/floating_bar_backing_view.h"
+#import "chrome/browser/ui/cocoa/framed_browser_window.h"
+#import "chrome/browser/ui/cocoa/fullscreen_controller.h"
+#import "chrome/browser/ui/cocoa/previewable_contents_controller.h"
+#import "chrome/browser/ui/cocoa/side_tab_strip_controller.h"
+#import "chrome/browser/ui/cocoa/tab_strip_controller.h"
+#import "chrome/browser/ui/cocoa/tab_strip_view.h"
+#import "chrome/browser/ui/cocoa/toolbar_controller.h"
+#include "chrome/common/pref_names.h"
+
+namespace {
+
+// Space between the incognito badge and the right edge of the window.
+const CGFloat kIncognitoBadgeOffset = 4;
+
+// Insets for the location bar, used when the full toolbar is hidden.
+// TODO(viettrungluu): We can argue about the "correct" insetting; I like the
+// following best, though arguably 0 inset is better/more correct.
+const CGFloat kLocBarLeftRightInset = 1;
+const CGFloat kLocBarTopInset = 0;
+const CGFloat kLocBarBottomInset = 1;
+
+} // end namespace
+
+
+@implementation BrowserWindowController(Private)
+
+// Create the appropriate tab strip controller based on whether or not side
+// tabs are enabled.
+- (void)createTabStripController {
+ Class factory = [TabStripController class];
+ if ([self useVerticalTabs])
+ factory = [SideTabStripController class];
+
+ DCHECK([previewableContentsController_ activeContainer]);
+ DCHECK([[previewableContentsController_ activeContainer] window]);
+ tabStripController_.reset([[factory alloc]
+ initWithView:[self tabStripView]
+ switchView:[previewableContentsController_ activeContainer]
+ browser:browser_.get()
+ delegate:self]);
+}
+
+- (void)saveWindowPositionIfNeeded {
+ if (browser_ != BrowserList::GetLastActive())
+ return;
+
+ if (!g_browser_process || !g_browser_process->local_state() ||
+ !browser_->ShouldSaveWindowPlacement())
+ return;
+
+ [self saveWindowPositionToPrefs:g_browser_process->local_state()];
+}
+
+- (void)saveWindowPositionToPrefs:(PrefService*)prefs {
+ // If we're in fullscreen mode, save the position of the regular window
+ // instead.
+ NSWindow* window = [self isFullscreen] ? savedRegularWindow_ : [self window];
+
+ // Window positions are stored relative to the origin of the primary monitor.
+ NSRect monitorFrame = [[[NSScreen screens] objectAtIndex:0] frame];
+ NSScreen* windowScreen = [window screen];
+
+ // |windowScreen| can be nil (for example, if the monitor arrangement was
+ // changed while in fullscreen mode). If we see a nil screen, return without
+ // saving.
+ // TODO(rohitrao): We should just not save anything for fullscreen windows.
+ // http://crbug.com/36479.
+ if (!windowScreen)
+ return;
+
+ // Start with the window's frame, which is in virtual coordinates.
+ // Do some y twiddling to flip the coordinate system.
+ gfx::Rect bounds(NSRectToCGRect([window frame]));
+ bounds.set_y(monitorFrame.size.height - bounds.y() - bounds.height());
+
+ // We also need to save the current work area, in flipped coordinates.
+ gfx::Rect workArea(NSRectToCGRect([windowScreen visibleFrame]));
+ workArea.set_y(monitorFrame.size.height - workArea.y() - workArea.height());
+
+ // Browser::SaveWindowPlacement is used for session restore.
+ if (browser_->ShouldSaveWindowPlacement())
+ browser_->SaveWindowPlacement(bounds, /*maximized=*/ false);
+
+ DictionaryValue* windowPreferences = prefs->GetMutableDictionary(
+ browser_->GetWindowPlacementKey().c_str());
+ windowPreferences->SetInteger("left", bounds.x());
+ windowPreferences->SetInteger("top", bounds.y());
+ windowPreferences->SetInteger("right", bounds.right());
+ windowPreferences->SetInteger("bottom", bounds.bottom());
+ windowPreferences->SetBoolean("maximized", false);
+ windowPreferences->SetBoolean("always_on_top", false);
+ windowPreferences->SetInteger("work_area_left", workArea.x());
+ windowPreferences->SetInteger("work_area_top", workArea.y());
+ windowPreferences->SetInteger("work_area_right", workArea.right());
+ windowPreferences->SetInteger("work_area_bottom", workArea.bottom());
+}
+
+- (NSRect)window:(NSWindow*)window
+willPositionSheet:(NSWindow*)sheet
+ usingRect:(NSRect)defaultSheetRect {
+ // Position the sheet as follows:
+ // - If the bookmark bar is hidden or shown as a bubble (on the NTP when the
+ // bookmark bar is disabled), position the sheet immediately below the
+ // normal toolbar.
+ // - If the bookmark bar is shown (attached to the normal toolbar), position
+ // the sheet below the bookmark bar.
+ // - If the bookmark bar is currently animating, position the sheet according
+ // to where the bar will be when the animation ends.
+ switch ([bookmarkBarController_ visualState]) {
+ case bookmarks::kShowingState: {
+ NSRect bookmarkBarFrame = [[bookmarkBarController_ view] frame];
+ defaultSheetRect.origin.y = bookmarkBarFrame.origin.y;
+ break;
+ }
+ case bookmarks::kHiddenState:
+ case bookmarks::kDetachedState: {
+ NSRect toolbarFrame = [[toolbarController_ view] frame];
+ defaultSheetRect.origin.y = toolbarFrame.origin.y;
+ break;
+ }
+ case bookmarks::kInvalidState:
+ default:
+ NOTREACHED();
+ }
+ return defaultSheetRect;
+}
+
+- (void)layoutSubviews {
+ // With the exception of the top tab strip, the subviews which we lay out are
+ // subviews of the content view, so we mainly work in the content view's
+ // coordinate system. Note, however, that the content view's coordinate system
+ // and the window's base coordinate system should coincide.
+ NSWindow* window = [self window];
+ NSView* contentView = [window contentView];
+ NSRect contentBounds = [contentView bounds];
+ CGFloat minX = NSMinX(contentBounds);
+ CGFloat minY = NSMinY(contentBounds);
+ CGFloat width = NSWidth(contentBounds);
+
+ // Suppress title drawing if necessary.
+ if ([window respondsToSelector:@selector(setShouldHideTitle:)])
+ [(id)window setShouldHideTitle:![self hasTitleBar]];
+
+ BOOL isFullscreen = [self isFullscreen];
+ CGFloat floatingBarHeight = [self floatingBarHeight];
+ // In fullscreen mode, |yOffset| accounts for the sliding position of the
+ // floating bar and the extra offset needed to dodge the menu bar.
+ CGFloat yOffset = isFullscreen ?
+ (floor((1 - floatingBarShownFraction_) * floatingBarHeight) -
+ [fullscreenController_ floatingBarVerticalOffset]) : 0;
+ CGFloat maxY = NSMaxY(contentBounds) + yOffset;
+ CGFloat startMaxY = maxY;
+
+ if ([self hasTabStrip] && ![self useVerticalTabs]) {
+ // If we need to lay out the top tab strip, replace |maxY| and |startMaxY|
+ // with higher values, and then lay out the tab strip.
+ NSRect windowFrame = [contentView convertRect:[window frame] fromView:nil];
+ startMaxY = maxY = NSHeight(windowFrame) + yOffset;
+ maxY = [self layoutTabStripAtMaxY:maxY width:width fullscreen:isFullscreen];
+ }
+
+ // Sanity-check |maxY|.
+ DCHECK_GE(maxY, minY);
+ DCHECK_LE(maxY, NSMaxY(contentBounds) + yOffset);
+
+ // The base class already positions the side tab strip on the left side
+ // of the window's content area and sizes it to take the entire vertical
+ // height. All that's needed here is to push everything over to the right,
+ // if necessary.
+ if ([self useVerticalTabs]) {
+ const CGFloat sideTabWidth = [[self tabStripView] bounds].size.width;
+ minX += sideTabWidth;
+ width -= sideTabWidth;
+ }
+
+ // Place the toolbar at the top of the reserved area.
+ maxY = [self layoutToolbarAtMinX:minX maxY:maxY width:width];
+
+ // If we're not displaying the bookmark bar below the infobar, then it goes
+ // immediately below the toolbar.
+ BOOL placeBookmarkBarBelowInfoBar = [self placeBookmarkBarBelowInfoBar];
+ if (!placeBookmarkBarBelowInfoBar)
+ maxY = [self layoutBookmarkBarAtMinX:minX maxY:maxY width:width];
+
+ // The floating bar backing view doesn't actually add any height.
+ NSRect floatingBarBackingRect =
+ NSMakeRect(minX, maxY, width, floatingBarHeight);
+ [self layoutFloatingBarBackingView:floatingBarBackingRect
+ fullscreen:isFullscreen];
+
+ // Place the find bar immediately below the toolbar/attached bookmark bar. In
+ // fullscreen mode, it hangs off the top of the screen when the bar is hidden.
+ // The find bar is unaffected by the side tab positioning.
+ [findBarCocoaController_ positionFindBarViewAtMaxY:maxY maxWidth:width];
+
+ // If in fullscreen mode, reset |maxY| to top of screen, so that the floating
+ // bar slides over the things which appear to be in the content area.
+ if (isFullscreen)
+ maxY = NSMaxY(contentBounds);
+
+ // Also place the infobar container immediate below the toolbar, except in
+ // fullscreen mode in which case it's at the top of the visual content area.
+ maxY = [self layoutInfoBarAtMinX:minX maxY:maxY width:width];
+
+ // If the bookmark bar is detached, place it next in the visual content area.
+ if (placeBookmarkBarBelowInfoBar)
+ maxY = [self layoutBookmarkBarAtMinX:minX maxY:maxY width:width];
+
+ // Place the download shelf, if any, at the bottom of the view.
+ minY = [self layoutDownloadShelfAtMinX:minX minY:minY width:width];
+
+ // Finally, the content area takes up all of the remaining space.
+ NSRect contentAreaRect = NSMakeRect(minX, minY, width, maxY - minY);
+ [self layoutTabContentArea:contentAreaRect];
+
+ // Normally, we don't need to tell the toolbar whether or not to show the
+ // divider, but things break down during animation.
+ [toolbarController_
+ setDividerOpacity:[bookmarkBarController_ toolbarDividerOpacity]];
+}
+
+- (CGFloat)floatingBarHeight {
+ if (![self isFullscreen])
+ return 0;
+
+ CGFloat totalHeight = [fullscreenController_ floatingBarVerticalOffset];
+
+ if ([self hasTabStrip])
+ totalHeight += NSHeight([[self tabStripView] frame]);
+
+ if ([self hasToolbar]) {
+ totalHeight += NSHeight([[toolbarController_ view] frame]);
+ } else if ([self hasLocationBar]) {
+ totalHeight += NSHeight([[toolbarController_ view] frame]) +
+ kLocBarTopInset + kLocBarBottomInset;
+ }
+
+ if (![self placeBookmarkBarBelowInfoBar])
+ totalHeight += NSHeight([[bookmarkBarController_ view] frame]);
+
+ return totalHeight;
+}
+
+- (CGFloat)layoutTabStripAtMaxY:(CGFloat)maxY
+ width:(CGFloat)width
+ fullscreen:(BOOL)fullscreen {
+ // Nothing to do if no tab strip.
+ if (![self hasTabStrip])
+ return maxY;
+
+ NSView* tabStripView = [self tabStripView];
+ CGFloat tabStripHeight = NSHeight([tabStripView frame]);
+ maxY -= tabStripHeight;
+ [tabStripView setFrame:NSMakeRect(0, maxY, width, tabStripHeight)];
+
+ // Set indentation.
+ [tabStripController_ setIndentForControls:(fullscreen ? 0 :
+ [[tabStripController_ class] defaultIndentForControls])];
+
+ // TODO(viettrungluu): Seems kind of bad -- shouldn't |-layoutSubviews| do
+ // this? Moreover, |-layoutTabs| will try to animate....
+ [tabStripController_ layoutTabs];
+
+ // Now lay out incognito badge together with the tab strip.
+ if (incognitoBadge_.get()) {
+ // Actually place the badge *above* |maxY|.
+ NSPoint origin = NSMakePoint(width - NSWidth([incognitoBadge_ frame]) -
+ kIncognitoBadgeOffset, maxY);
+ [incognitoBadge_ setFrameOrigin:origin];
+ [incognitoBadge_ setHidden:NO]; // Make sure it's shown.
+ }
+
+ return maxY;
+}
+
+- (CGFloat)layoutToolbarAtMinX:(CGFloat)minX
+ maxY:(CGFloat)maxY
+ width:(CGFloat)width {
+ NSView* toolbarView = [toolbarController_ view];
+ NSRect toolbarFrame = [toolbarView frame];
+ if ([self hasToolbar]) {
+ // The toolbar is present in the window, so we make room for it.
+ DCHECK(![toolbarView isHidden]);
+ toolbarFrame.origin.x = minX;
+ toolbarFrame.origin.y = maxY - NSHeight(toolbarFrame);
+ toolbarFrame.size.width = width;
+ maxY -= NSHeight(toolbarFrame);
+ } else {
+ if ([self hasLocationBar]) {
+ // Location bar is present with no toolbar. Put a border of
+ // |kLocBar...Inset| pixels around the location bar.
+ // TODO(viettrungluu): This is moderately ridiculous. The toolbar should
+ // really be aware of what its height should be (the way the toolbar
+ // compression stuff is currently set up messes things up).
+ DCHECK(![toolbarView isHidden]);
+ toolbarFrame.origin.x = kLocBarLeftRightInset;
+ toolbarFrame.origin.y = maxY - NSHeight(toolbarFrame) - kLocBarTopInset;
+ toolbarFrame.size.width = width - 2 * kLocBarLeftRightInset;
+ maxY -= kLocBarTopInset + NSHeight(toolbarFrame) + kLocBarBottomInset;
+ } else {
+ DCHECK([toolbarView isHidden]);
+ }
+ }
+ [toolbarView setFrame:toolbarFrame];
+ return maxY;
+}
+
+- (BOOL)placeBookmarkBarBelowInfoBar {
+ // If we are currently displaying the NTP detached bookmark bar or animating
+ // to/from it (from/to anything else), we display the bookmark bar below the
+ // infobar.
+ return [bookmarkBarController_ isInState:bookmarks::kDetachedState] ||
+ [bookmarkBarController_ isAnimatingToState:bookmarks::kDetachedState] ||
+ [bookmarkBarController_ isAnimatingFromState:bookmarks::kDetachedState];
+}
+
+- (CGFloat)layoutBookmarkBarAtMinX:(CGFloat)minX
+ maxY:(CGFloat)maxY
+ width:(CGFloat)width {
+ NSView* bookmarkBarView = [bookmarkBarController_ view];
+ NSRect bookmarkBarFrame = [bookmarkBarView frame];
+ BOOL oldHidden = [bookmarkBarView isHidden];
+ BOOL newHidden = ![self isBookmarkBarVisible];
+ if (oldHidden != newHidden)
+ [bookmarkBarView setHidden:newHidden];
+ bookmarkBarFrame.origin.x = minX;
+ bookmarkBarFrame.origin.y = maxY - NSHeight(bookmarkBarFrame);
+ bookmarkBarFrame.size.width = width;
+ [bookmarkBarView setFrame:bookmarkBarFrame];
+ maxY -= NSHeight(bookmarkBarFrame);
+
+ // TODO(viettrungluu): Does this really belong here? Calling it shouldn't be
+ // necessary in the non-NTP case.
+ [bookmarkBarController_ layoutSubviews];
+
+ return maxY;
+}
+
+- (void)layoutFloatingBarBackingView:(NSRect)frame
+ fullscreen:(BOOL)fullscreen {
+ // Only display when in fullscreen mode.
+ if (fullscreen) {
+ // For certain window types such as app windows (e.g., the dev tools
+ // window), there's no actual overlay. (Displaying one would result in an
+ // overly sliding in only under the menu, which gives an ugly effect.)
+ if (floatingBarBackingView_.get()) {
+ BOOL aboveBookmarkBar = [self placeBookmarkBarBelowInfoBar];
+
+ // Insert it into the view hierarchy if necessary.
+ if (![floatingBarBackingView_ superview] ||
+ aboveBookmarkBar != floatingBarAboveBookmarkBar_) {
+ NSView* contentView = [[self window] contentView];
+ // z-order gets messed up unless we explicitly remove the floatingbar
+ // view and re-add it.
+ [floatingBarBackingView_ removeFromSuperview];
+ [contentView addSubview:floatingBarBackingView_
+ positioned:(aboveBookmarkBar ?
+ NSWindowAbove : NSWindowBelow)
+ relativeTo:[bookmarkBarController_ view]];
+ floatingBarAboveBookmarkBar_ = aboveBookmarkBar;
+ }
+
+ // Set its frame.
+ [floatingBarBackingView_ setFrame:frame];
+ }
+
+ // But we want the logic to work as usual (for show/hide/etc. purposes).
+ [fullscreenController_ overlayFrameChanged:frame];
+ } else {
+ // Okay to call even if |floatingBarBackingView_| is nil.
+ if ([floatingBarBackingView_ superview])
+ [floatingBarBackingView_ removeFromSuperview];
+ }
+}
+
+- (CGFloat)layoutInfoBarAtMinX:(CGFloat)minX
+ maxY:(CGFloat)maxY
+ width:(CGFloat)width {
+ NSView* infoBarView = [infoBarContainerController_ view];
+ NSRect infoBarFrame = [infoBarView frame];
+ infoBarFrame.origin.x = minX;
+ infoBarFrame.origin.y = maxY - NSHeight(infoBarFrame);
+ infoBarFrame.size.width = width;
+ [infoBarView setFrame:infoBarFrame];
+ maxY -= NSHeight(infoBarFrame);
+ return maxY;
+}
+
+- (CGFloat)layoutDownloadShelfAtMinX:(CGFloat)minX
+ minY:(CGFloat)minY
+ width:(CGFloat)width {
+ if (downloadShelfController_.get()) {
+ NSView* downloadView = [downloadShelfController_ view];
+ NSRect downloadFrame = [downloadView frame];
+ downloadFrame.origin.x = minX;
+ downloadFrame.origin.y = minY;
+ downloadFrame.size.width = width;
+ [downloadView setFrame:downloadFrame];
+ minY += NSHeight(downloadFrame);
+ }
+ return minY;
+}
+
+- (void)layoutTabContentArea:(NSRect)newFrame {
+ NSView* tabContentView = [self tabContentArea];
+ NSRect tabContentFrame = [tabContentView frame];
+
+ bool contentShifted =
+ NSMaxY(tabContentFrame) != NSMaxY(newFrame) ||
+ NSMinX(tabContentFrame) != NSMinX(newFrame);
+
+ tabContentFrame = newFrame;
+ [tabContentView setFrame:tabContentFrame];
+
+ // If the relayout shifts the content area up or down, let the renderer know.
+ if (contentShifted) {
+ if (TabContents* contents = browser_->GetSelectedTabContents()) {
+ if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView())
+ rwhv->WindowFrameChanged();
+ }
+ }
+}
+
+- (BOOL)shouldShowBookmarkBar {
+ DCHECK(browser_.get());
+ return browser_->profile()->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar) ?
+ YES : NO;
+}
+
+- (BOOL)shouldShowDetachedBookmarkBar {
+ DCHECK(browser_.get());
+ TabContents* contents = browser_->GetSelectedTabContents();
+ return (contents &&
+ contents->ShouldShowBookmarkBar() &&
+ ![previewableContentsController_ isShowingPreview]);
+}
+
+- (void)adjustToolbarAndBookmarkBarForCompression:(CGFloat)compression {
+ CGFloat newHeight =
+ [toolbarController_ desiredHeightForCompression:compression];
+ NSRect toolbarFrame = [[toolbarController_ view] frame];
+ CGFloat deltaH = newHeight - toolbarFrame.size.height;
+
+ if (deltaH == 0)
+ return;
+
+ toolbarFrame.size.height = newHeight;
+ NSRect bookmarkFrame = [[bookmarkBarController_ view] frame];
+ bookmarkFrame.size.height = bookmarkFrame.size.height - deltaH;
+ [[toolbarController_ view] setFrame:toolbarFrame];
+ [[bookmarkBarController_ view] setFrame:bookmarkFrame];
+ [self layoutSubviews];
+}
+
+// TODO(rohitrao): This function has shrunk into uselessness, and
+// |-setFullscreen:| has grown rather large. Find a good way to break up
+// |-setFullscreen:| into smaller pieces. http://crbug.com/36449
+- (void)adjustUIForFullscreen:(BOOL)fullscreen {
+ // Create the floating bar backing view if necessary.
+ if (fullscreen && !floatingBarBackingView_.get() &&
+ ([self hasTabStrip] || [self hasToolbar] || [self hasLocationBar])) {
+ floatingBarBackingView_.reset(
+ [[FloatingBarBackingView alloc] initWithFrame:NSZeroRect]);
+ }
+}
+
+- (void)enableBarVisibilityUpdates {
+ // Early escape if there's nothing to do.
+ if (barVisibilityUpdatesEnabled_)
+ return;
+
+ barVisibilityUpdatesEnabled_ = YES;
+
+ if ([barVisibilityLocks_ count])
+ [fullscreenController_ ensureOverlayShownWithAnimation:NO delay:NO];
+ else
+ [fullscreenController_ ensureOverlayHiddenWithAnimation:NO delay:NO];
+}
+
+- (void)disableBarVisibilityUpdates {
+ // Early escape if there's nothing to do.
+ if (!barVisibilityUpdatesEnabled_)
+ return;
+
+ barVisibilityUpdatesEnabled_ = NO;
+ [fullscreenController_ cancelAnimationAndTimers];
+}
+
+@end // @implementation BrowserWindowController(Private)
diff --git a/chrome/browser/ui/cocoa/browser_window_controller_unittest.mm b/chrome/browser/ui/cocoa/browser_window_controller_unittest.mm
new file mode 100644
index 0000000..742b7f4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/browser_window_controller_unittest.mm
@@ -0,0 +1,670 @@
+// 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 "app/l10n_util_mac.h"
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/browser_window.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/sync/sync_ui_util.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/browser_window_controller.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/find_bar_bridge.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/testing_browser_process.h"
+#include "chrome/test/testing_profile.h"
+#include "grit/generated_resources.h"
+
+@interface BrowserWindowController (JustForTesting)
+// Already defined in BWC.
+- (void)saveWindowPositionToPrefs:(PrefService*)prefs;
+- (void)layoutSubviews;
+@end
+
+@interface BrowserWindowController (ExposedForTesting)
+// Implementations are below.
+- (NSView*)infoBarContainerView;
+- (NSView*)toolbarView;
+- (NSView*)bookmarkView;
+- (BOOL)bookmarkBarVisible;
+@end
+
+@implementation BrowserWindowController (ExposedForTesting)
+- (NSView*)infoBarContainerView {
+ return [infoBarContainerController_ view];
+}
+
+- (NSView*)toolbarView {
+ return [toolbarController_ view];
+}
+
+- (NSView*)bookmarkView {
+ return [bookmarkBarController_ view];
+}
+
+- (NSView*)findBarView {
+ return [findBarCocoaController_ view];
+}
+
+- (NSSplitView*)devToolsView {
+ return static_cast<NSSplitView*>([devToolsController_ view]);
+}
+
+- (NSView*)sidebarView {
+ return [sidebarController_ view];
+}
+
+- (BOOL)bookmarkBarVisible {
+ return [bookmarkBarController_ isVisible];
+}
+@end
+
+class BrowserWindowControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ Browser* browser = browser_helper_.browser();
+ controller_ = [[BrowserWindowController alloc] initWithBrowser:browser
+ takeOwnership:NO];
+ }
+
+ virtual void TearDown() {
+ [controller_ close];
+ CocoaTest::TearDown();
+ }
+
+ public:
+ BrowserTestHelper browser_helper_;
+ BrowserWindowController* controller_;
+};
+
+TEST_F(BrowserWindowControllerTest, TestSaveWindowPosition) {
+ PrefService* prefs = browser_helper_.profile()->GetPrefs();
+ ASSERT_TRUE(prefs != NULL);
+
+ // Check to make sure there is no existing pref for window placement.
+ ASSERT_TRUE(prefs->GetDictionary(prefs::kBrowserWindowPlacement) == NULL);
+
+ // Ask the window to save its position, then check that a preference
+ // exists. We're technically passing in a pointer to the user prefs
+ // and not the local state prefs, but a PrefService* is a
+ // PrefService*, and this is a unittest.
+ [controller_ saveWindowPositionToPrefs:prefs];
+ EXPECT_TRUE(prefs->GetDictionary(prefs::kBrowserWindowPlacement) != NULL);
+}
+
+TEST_F(BrowserWindowControllerTest, TestFullScreenWindow) {
+ // Confirm that |-createFullscreenWindow| doesn't return nil.
+ // See BrowserWindowFullScreenControllerTest for more fullscreen tests.
+ EXPECT_TRUE([controller_ createFullscreenWindow]);
+}
+
+TEST_F(BrowserWindowControllerTest, TestNormal) {
+ // Force the bookmark bar to be shown.
+ browser_helper_.profile()->GetPrefs()->
+ SetBoolean(prefs::kShowBookmarkBar, true);
+ [controller_ updateBookmarkBarVisibilityWithAnimation:NO];
+
+ // Make sure a normal BrowserWindowController is, uh, normal.
+ EXPECT_TRUE([controller_ isNormalWindow]);
+ EXPECT_TRUE([controller_ hasTabStrip]);
+ EXPECT_FALSE([controller_ hasTitleBar]);
+ EXPECT_TRUE([controller_ isBookmarkBarVisible]);
+
+ // And make sure a controller for a pop-up window is not normal.
+ // popup_browser will be owned by its window.
+ Browser *popup_browser(Browser::CreateForType(Browser::TYPE_POPUP,
+ browser_helper_.profile()));
+ NSWindow *cocoaWindow = popup_browser->window()->GetNativeHandle();
+ BrowserWindowController* controller =
+ static_cast<BrowserWindowController*>([cocoaWindow windowController]);
+ ASSERT_TRUE([controller isKindOfClass:[BrowserWindowController class]]);
+ EXPECT_FALSE([controller isNormalWindow]);
+ EXPECT_FALSE([controller hasTabStrip]);
+ EXPECT_TRUE([controller hasTitleBar]);
+ EXPECT_FALSE([controller isBookmarkBarVisible]);
+ [controller close];
+}
+
+TEST_F(BrowserWindowControllerTest, TestTheme) {
+ [controller_ userChangedTheme];
+}
+
+TEST_F(BrowserWindowControllerTest, BookmarkBarControllerIndirection) {
+ EXPECT_FALSE([controller_ isBookmarkBarVisible]);
+
+ // Explicitly show the bar. Can't use bookmark_utils::ToggleWhenVisible()
+ // because of the notification issues.
+ browser_helper_.profile()->GetPrefs()->
+ SetBoolean(prefs::kShowBookmarkBar, true);
+
+ [controller_ updateBookmarkBarVisibilityWithAnimation:NO];
+ EXPECT_TRUE([controller_ isBookmarkBarVisible]);
+}
+
+#if 0
+// TODO(jrg): This crashes trying to create the BookmarkBarController, adding
+// an observer to the BookmarkModel.
+TEST_F(BrowserWindowControllerTest, TestIncognitoWidthSpace) {
+ scoped_ptr<TestingProfile> incognito_profile(new TestingProfile());
+ incognito_profile->set_off_the_record(true);
+ scoped_ptr<Browser> browser(new Browser(Browser::TYPE_NORMAL,
+ incognito_profile.get()));
+ controller_.reset([[BrowserWindowController alloc]
+ initWithBrowser:browser.get()
+ takeOwnership:NO]);
+
+ NSRect tabFrame = [[controller_ tabStripView] frame];
+ [controller_ installIncognitoBadge];
+ NSRect newTabFrame = [[controller_ tabStripView] frame];
+ EXPECT_GT(tabFrame.size.width, newTabFrame.size.width);
+
+ controller_.release();
+}
+#endif
+
+namespace {
+// Verifies that the toolbar, infobar, tab content area, and download shelf
+// completely fill the area under the tabstrip.
+void CheckViewPositions(BrowserWindowController* controller) {
+ NSRect contentView = [[[controller window] contentView] bounds];
+ NSRect tabstrip = [[controller tabStripView] frame];
+ NSRect toolbar = [[controller toolbarView] frame];
+ NSRect infobar = [[controller infoBarContainerView] frame];
+ NSRect contentArea = [[controller tabContentArea] frame];
+ NSRect download = [[[controller downloadShelf] view] frame];
+
+ EXPECT_EQ(NSMinY(contentView), NSMinY(download));
+ EXPECT_EQ(NSMaxY(download), NSMinY(contentArea));
+ EXPECT_EQ(NSMaxY(contentArea), NSMinY(infobar));
+
+ // Bookmark bar frame is random memory when hidden.
+ if ([controller bookmarkBarVisible]) {
+ NSRect bookmark = [[controller bookmarkView] frame];
+ EXPECT_EQ(NSMaxY(infobar), NSMinY(bookmark));
+ EXPECT_EQ(NSMaxY(bookmark), NSMinY(toolbar));
+ EXPECT_FALSE([[controller bookmarkView] isHidden]);
+ } else {
+ EXPECT_EQ(NSMaxY(infobar), NSMinY(toolbar));
+ EXPECT_TRUE([[controller bookmarkView] isHidden]);
+ }
+
+ // Toolbar should start immediately under the tabstrip, but the tabstrip is
+ // not necessarily fixed with respect to the content view.
+ EXPECT_EQ(NSMinY(tabstrip), NSMaxY(toolbar));
+}
+} // end namespace
+
+TEST_F(BrowserWindowControllerTest, TestAdjustWindowHeight) {
+ NSWindow* window = [controller_ window];
+ NSRect workarea = [[window screen] visibleFrame];
+
+ // Place the window well above the bottom of the screen and try to adjust its
+ // height. It should change appropriately (and only downwards). Then get it to
+ // shrink by the same amount; it should return to its original state.
+ NSRect initialFrame = NSMakeRect(workarea.origin.x, workarea.origin.y + 100,
+ 200, 200);
+ [window setFrame:initialFrame display:YES];
+ [controller_ resetWindowGrowthState];
+ [controller_ adjustWindowHeightBy:40];
+ NSRect finalFrame = [window frame];
+ EXPECT_FALSE(NSEqualRects(finalFrame, initialFrame));
+ EXPECT_FLOAT_EQ(NSMaxY(finalFrame), NSMaxY(initialFrame));
+ EXPECT_FLOAT_EQ(NSHeight(finalFrame), NSHeight(initialFrame) + 40);
+ [controller_ adjustWindowHeightBy:-40];
+ finalFrame = [window frame];
+ EXPECT_FLOAT_EQ(NSMaxY(finalFrame), NSMaxY(initialFrame));
+ EXPECT_FLOAT_EQ(NSHeight(finalFrame), NSHeight(initialFrame));
+
+ // Place the window at the bottom of the screen and try again. Its height
+ // should still change, but it should not grow down below the work area; it
+ // should instead move upwards. Then shrink it and make sure it goes back to
+ // the way it was.
+ initialFrame = NSMakeRect(workarea.origin.x, workarea.origin.y, 200, 200);
+ [window setFrame:initialFrame display:YES];
+ [controller_ resetWindowGrowthState];
+ [controller_ adjustWindowHeightBy:40];
+ finalFrame = [window frame];
+ EXPECT_FALSE(NSEqualRects(finalFrame, initialFrame));
+ EXPECT_FLOAT_EQ(NSMinY(finalFrame), NSMinY(initialFrame));
+ EXPECT_FLOAT_EQ(NSHeight(finalFrame), NSHeight(initialFrame) + 40);
+ [controller_ adjustWindowHeightBy:-40];
+ finalFrame = [window frame];
+ EXPECT_FLOAT_EQ(NSMinY(finalFrame), NSMinY(initialFrame));
+ EXPECT_FLOAT_EQ(NSHeight(finalFrame), NSHeight(initialFrame));
+
+ // Put the window slightly offscreen and try again. The height should not
+ // change this time.
+ initialFrame = NSMakeRect(workarea.origin.x - 10, 0, 200, 200);
+ [window setFrame:initialFrame display:YES];
+ [controller_ resetWindowGrowthState];
+ [controller_ adjustWindowHeightBy:40];
+ EXPECT_TRUE(NSEqualRects([window frame], initialFrame));
+ [controller_ adjustWindowHeightBy:-40];
+ EXPECT_TRUE(NSEqualRects([window frame], initialFrame));
+
+ // Make the window the same size as the workarea. Resizing both larger and
+ // smaller should have no effect.
+ [window setFrame:workarea display:YES];
+ [controller_ resetWindowGrowthState];
+ [controller_ adjustWindowHeightBy:40];
+ EXPECT_TRUE(NSEqualRects([window frame], workarea));
+ [controller_ adjustWindowHeightBy:-40];
+ EXPECT_TRUE(NSEqualRects([window frame], workarea));
+
+ // Make the window smaller than the workarea and place it near the bottom of
+ // the workarea. The window should grow down until it hits the bottom and
+ // then continue to grow up. Then shrink it, and it should return to where it
+ // was.
+ initialFrame = NSMakeRect(workarea.origin.x, workarea.origin.y + 5,
+ 200, 200);
+ [window setFrame:initialFrame display:YES];
+ [controller_ resetWindowGrowthState];
+ [controller_ adjustWindowHeightBy:40];
+ finalFrame = [window frame];
+ EXPECT_FLOAT_EQ(NSMinY(workarea), NSMinY(finalFrame));
+ EXPECT_FLOAT_EQ(NSHeight(finalFrame), NSHeight(initialFrame) + 40);
+ [controller_ adjustWindowHeightBy:-40];
+ finalFrame = [window frame];
+ EXPECT_FLOAT_EQ(NSMinY(initialFrame), NSMinY(finalFrame));
+ EXPECT_FLOAT_EQ(NSHeight(initialFrame), NSHeight(finalFrame));
+
+ // Inset the window slightly from the workarea. It should not grow to be
+ // larger than the workarea. Shrink it; it should return to where it started.
+ initialFrame = NSInsetRect(workarea, 0, 5);
+ [window setFrame:initialFrame display:YES];
+ [controller_ resetWindowGrowthState];
+ [controller_ adjustWindowHeightBy:40];
+ finalFrame = [window frame];
+ EXPECT_FLOAT_EQ(NSMinY(workarea), NSMinY(finalFrame));
+ EXPECT_FLOAT_EQ(NSHeight(workarea), NSHeight(finalFrame));
+ [controller_ adjustWindowHeightBy:-40];
+ finalFrame = [window frame];
+ EXPECT_FLOAT_EQ(NSMinY(initialFrame), NSMinY(finalFrame));
+ EXPECT_FLOAT_EQ(NSHeight(initialFrame), NSHeight(finalFrame));
+
+ // Place the window at the bottom of the screen and grow; it should grow
+ // upwards. Move the window off the bottom, then shrink. It should then shrink
+ // from the bottom.
+ initialFrame = NSMakeRect(workarea.origin.x, workarea.origin.y, 200, 200);
+ [window setFrame:initialFrame display:YES];
+ [controller_ resetWindowGrowthState];
+ [controller_ adjustWindowHeightBy:40];
+ finalFrame = [window frame];
+ EXPECT_FALSE(NSEqualRects(finalFrame, initialFrame));
+ EXPECT_FLOAT_EQ(NSMinY(finalFrame), NSMinY(initialFrame));
+ EXPECT_FLOAT_EQ(NSHeight(finalFrame), NSHeight(initialFrame) + 40);
+ NSPoint oldOrigin = initialFrame.origin;
+ NSPoint newOrigin = NSMakePoint(oldOrigin.x, oldOrigin.y + 10);
+ [window setFrameOrigin:newOrigin];
+ initialFrame = [window frame];
+ EXPECT_FLOAT_EQ(NSMinY(initialFrame), oldOrigin.y + 10);
+ [controller_ adjustWindowHeightBy:-40];
+ finalFrame = [window frame];
+ EXPECT_FLOAT_EQ(NSMinY(finalFrame), NSMinY(initialFrame) + 40);
+ EXPECT_FLOAT_EQ(NSHeight(finalFrame), NSHeight(initialFrame) - 40);
+
+ // Do the "inset" test above, but using multiple calls to
+ // |-adjustWindowHeightBy|; the result should be the same.
+ initialFrame = NSInsetRect(workarea, 0, 5);
+ [window setFrame:initialFrame display:YES];
+ [controller_ resetWindowGrowthState];
+ for (int i = 0; i < 8; i++)
+ [controller_ adjustWindowHeightBy:5];
+ finalFrame = [window frame];
+ EXPECT_FLOAT_EQ(NSMinY(workarea), NSMinY(finalFrame));
+ EXPECT_FLOAT_EQ(NSHeight(workarea), NSHeight(finalFrame));
+ for (int i = 0; i < 8; i++)
+ [controller_ adjustWindowHeightBy:-5];
+ finalFrame = [window frame];
+ EXPECT_FLOAT_EQ(NSMinY(initialFrame), NSMinY(finalFrame));
+ EXPECT_FLOAT_EQ(NSHeight(initialFrame), NSHeight(finalFrame));
+}
+
+// Test to make sure resizing and relaying-out subviews works correctly.
+TEST_F(BrowserWindowControllerTest, TestResizeViews) {
+ TabStripView* tabstrip = [controller_ tabStripView];
+ NSView* contentView = [[tabstrip window] contentView];
+ NSView* toolbar = [controller_ toolbarView];
+ NSView* infobar = [controller_ infoBarContainerView];
+
+ // We need to muck with the views a bit to put us in a consistent state before
+ // we start resizing. In particular, we need to move the tab strip to be
+ // immediately above the content area, since we layout views to be directly
+ // under the tab strip.
+ NSRect tabstripFrame = [tabstrip frame];
+ tabstripFrame.origin.y = NSMaxY([contentView frame]);
+ [tabstrip setFrame:tabstripFrame];
+
+ // The download shelf is created lazily. Force-create it and set its initial
+ // height to 0.
+ NSView* download = [[controller_ downloadShelf] view];
+ NSRect downloadFrame = [download frame];
+ downloadFrame.size.height = 0;
+ [download setFrame:downloadFrame];
+
+ // Force a layout and check each view's frame.
+ [controller_ layoutSubviews];
+ CheckViewPositions(controller_);
+
+ // Expand the infobar to 60px and recheck
+ [controller_ resizeView:infobar newHeight:60];
+ CheckViewPositions(controller_);
+
+ // Expand the toolbar to 64px and recheck
+ [controller_ resizeView:toolbar newHeight:64];
+ CheckViewPositions(controller_);
+
+ // Add a 30px download shelf and recheck
+ [controller_ resizeView:download newHeight:30];
+ CheckViewPositions(controller_);
+
+ // Shrink the infobar to 0px and toolbar to 39px and recheck
+ [controller_ resizeView:infobar newHeight:0];
+ [controller_ resizeView:toolbar newHeight:39];
+ CheckViewPositions(controller_);
+}
+
+TEST_F(BrowserWindowControllerTest, TestResizeViewsWithBookmarkBar) {
+ // Force a display of the bookmark bar.
+ browser_helper_.profile()->GetPrefs()->
+ SetBoolean(prefs::kShowBookmarkBar, true);
+ [controller_ updateBookmarkBarVisibilityWithAnimation:NO];
+
+ TabStripView* tabstrip = [controller_ tabStripView];
+ NSView* contentView = [[tabstrip window] contentView];
+ NSView* toolbar = [controller_ toolbarView];
+ NSView* bookmark = [controller_ bookmarkView];
+ NSView* infobar = [controller_ infoBarContainerView];
+
+ // We need to muck with the views a bit to put us in a consistent state before
+ // we start resizing. In particular, we need to move the tab strip to be
+ // immediately above the content area, since we layout views to be directly
+ // under the tab strip.
+ NSRect tabstripFrame = [tabstrip frame];
+ tabstripFrame.origin.y = NSMaxY([contentView frame]);
+ [tabstrip setFrame:tabstripFrame];
+
+ // The download shelf is created lazily. Force-create it and set its initial
+ // height to 0.
+ NSView* download = [[controller_ downloadShelf] view];
+ NSRect downloadFrame = [download frame];
+ downloadFrame.size.height = 0;
+ [download setFrame:downloadFrame];
+
+ // Force a layout and check each view's frame.
+ [controller_ layoutSubviews];
+ CheckViewPositions(controller_);
+
+ // Add the bookmark bar and recheck.
+ [controller_ resizeView:bookmark newHeight:40];
+ CheckViewPositions(controller_);
+
+ // Expand the infobar to 60px and recheck
+ [controller_ resizeView:infobar newHeight:60];
+ CheckViewPositions(controller_);
+
+ // Expand the toolbar to 64px and recheck
+ [controller_ resizeView:toolbar newHeight:64];
+ CheckViewPositions(controller_);
+
+ // Add a 30px download shelf and recheck
+ [controller_ resizeView:download newHeight:30];
+ CheckViewPositions(controller_);
+
+ // Remove the bookmark bar and recheck
+ browser_helper_.profile()->GetPrefs()->
+ SetBoolean(prefs::kShowBookmarkBar, false);
+ [controller_ resizeView:bookmark newHeight:0];
+ CheckViewPositions(controller_);
+
+ // Shrink the infobar to 0px and toolbar to 39px and recheck
+ [controller_ resizeView:infobar newHeight:0];
+ [controller_ resizeView:toolbar newHeight:39];
+ CheckViewPositions(controller_);
+}
+
+// Make sure, by default, the bookmark bar and the toolbar are the same width.
+TEST_F(BrowserWindowControllerTest, BookmarkBarIsSameWidth) {
+ // Set the pref to the bookmark bar is visible when the toolbar is
+ // first created.
+ browser_helper_.profile()->GetPrefs()->SetBoolean(
+ prefs::kShowBookmarkBar, true);
+
+ // Make sure the bookmark bar is the same width as the toolbar
+ NSView* bookmarkBarView = [controller_ bookmarkView];
+ NSView* toolbarView = [controller_ toolbarView];
+ EXPECT_EQ([toolbarView frame].size.width,
+ [bookmarkBarView frame].size.width);
+}
+
+TEST_F(BrowserWindowControllerTest, TestTopRightForBubble) {
+ NSPoint p = [controller_ bookmarkBubblePoint];
+ NSRect all = [[controller_ window] frame];
+
+ // As a sanity check make sure the point is vaguely in the top right
+ // of the window.
+ EXPECT_GT(p.y, all.origin.y + (all.size.height/2));
+ EXPECT_GT(p.x, all.origin.x + (all.size.width/2));
+}
+
+// By the "zoom frame", we mean what Apple calls the "standard frame".
+TEST_F(BrowserWindowControllerTest, TestZoomFrame) {
+ NSWindow* window = [controller_ window];
+ ASSERT_TRUE(window);
+ NSRect screenFrame = [[window screen] visibleFrame];
+ ASSERT_FALSE(NSIsEmptyRect(screenFrame));
+
+ // Minimum zoomed width is the larger of 60% of available horizontal space or
+ // 60% of available vertical space, subject to available horizontal space.
+ CGFloat minZoomWidth =
+ std::min(std::max((CGFloat)0.6 * screenFrame.size.width,
+ (CGFloat)0.6 * screenFrame.size.height),
+ screenFrame.size.width);
+
+ // |testFrame| is the size of the window we start out with, and |zoomFrame| is
+ // the one returned by |-windowWillUseStandardFrame:defaultFrame:|.
+ NSRect testFrame;
+ NSRect zoomFrame;
+
+ // 1. Test a case where it zooms the window both horizontally and vertically,
+ // and only moves it vertically. "+ 32", etc. are just arbitrary constants
+ // used to check that the window is moved properly and not just to the origin;
+ // they should be small enough to not shove windows off the screen.
+ testFrame.size.width = 0.5 * minZoomWidth;
+ testFrame.size.height = 0.5 * screenFrame.size.height;
+ testFrame.origin.x = screenFrame.origin.x + 32; // See above.
+ testFrame.origin.y = screenFrame.origin.y + 23;
+ [window setFrame:testFrame display:NO];
+ zoomFrame = [controller_ windowWillUseStandardFrame:window
+ defaultFrame:screenFrame];
+ EXPECT_LE(minZoomWidth, zoomFrame.size.width);
+ EXPECT_EQ(screenFrame.size.height, zoomFrame.size.height);
+ EXPECT_EQ(testFrame.origin.x, zoomFrame.origin.x);
+ EXPECT_EQ(screenFrame.origin.y, zoomFrame.origin.y);
+
+ // 2. Test a case where it zooms the window only horizontally, and only moves
+ // it horizontally.
+ testFrame.size.width = 0.5 * minZoomWidth;
+ testFrame.size.height = screenFrame.size.height;
+ testFrame.origin.x = screenFrame.origin.x + screenFrame.size.width -
+ testFrame.size.width;
+ testFrame.origin.y = screenFrame.origin.y;
+ [window setFrame:testFrame display:NO];
+ zoomFrame = [controller_ windowWillUseStandardFrame:window
+ defaultFrame:screenFrame];
+ EXPECT_LE(minZoomWidth, zoomFrame.size.width);
+ EXPECT_EQ(screenFrame.size.height, zoomFrame.size.height);
+ EXPECT_EQ(screenFrame.origin.x + screenFrame.size.width -
+ zoomFrame.size.width, zoomFrame.origin.x);
+ EXPECT_EQ(screenFrame.origin.y, zoomFrame.origin.y);
+
+ // 3. Test a case where it zooms the window only vertically, and only moves it
+ // vertically.
+ testFrame.size.width = std::min((CGFloat)1.1 * minZoomWidth,
+ screenFrame.size.width);
+ testFrame.size.height = 0.3 * screenFrame.size.height;
+ testFrame.origin.x = screenFrame.origin.x + 32; // See above (in 1.).
+ testFrame.origin.y = screenFrame.origin.y + 123;
+ [window setFrame:testFrame display:NO];
+ zoomFrame = [controller_ windowWillUseStandardFrame:window
+ defaultFrame:screenFrame];
+ // Use the actual width of the window frame, since it's subject to rounding.
+ EXPECT_EQ([window frame].size.width, zoomFrame.size.width);
+ EXPECT_EQ(screenFrame.size.height, zoomFrame.size.height);
+ EXPECT_EQ(testFrame.origin.x, zoomFrame.origin.x);
+ EXPECT_EQ(screenFrame.origin.y, zoomFrame.origin.y);
+
+ // 4. Test a case where zooming should do nothing (i.e., we're already at a
+ // zoomed frame).
+ testFrame.size.width = std::min((CGFloat)1.1 * minZoomWidth,
+ screenFrame.size.width);
+ testFrame.size.height = screenFrame.size.height;
+ testFrame.origin.x = screenFrame.origin.x + 32; // See above (in 1.).
+ testFrame.origin.y = screenFrame.origin.y;
+ [window setFrame:testFrame display:NO];
+ zoomFrame = [controller_ windowWillUseStandardFrame:window
+ defaultFrame:screenFrame];
+ // Use the actual width of the window frame, since it's subject to rounding.
+ EXPECT_EQ([window frame].size.width, zoomFrame.size.width);
+ EXPECT_EQ(screenFrame.size.height, zoomFrame.size.height);
+ EXPECT_EQ(testFrame.origin.x, zoomFrame.origin.x);
+ EXPECT_EQ(screenFrame.origin.y, zoomFrame.origin.y);
+}
+
+TEST_F(BrowserWindowControllerTest, TestFindBarOnTop) {
+ FindBarBridge bridge;
+ [controller_ addFindBar:bridge.find_bar_cocoa_controller()];
+
+ // Test that the Z-order of the find bar is on top of everything.
+ NSArray* subviews = [[[controller_ window] contentView] subviews];
+ NSUInteger findBar_index =
+ [subviews indexOfObject:[controller_ findBarView]];
+ EXPECT_NE(NSNotFound, findBar_index);
+ NSUInteger toolbar_index =
+ [subviews indexOfObject:[controller_ toolbarView]];
+ EXPECT_NE(NSNotFound, toolbar_index);
+ NSUInteger bookmark_index =
+ [subviews indexOfObject:[controller_ bookmarkView]];
+ EXPECT_NE(NSNotFound, bookmark_index);
+
+ EXPECT_GT(findBar_index, toolbar_index);
+ EXPECT_GT(findBar_index, bookmark_index);
+}
+
+// Tests that the sidebar view and devtools view are both non-opaque.
+TEST_F(BrowserWindowControllerTest, TestSplitViewsAreNotOpaque) {
+ // Add a subview to the sidebar view to mimic what happens when a tab is added
+ // to the window. NSSplitView only marks itself as non-opaque when one of its
+ // subviews is non-opaque, so the test will not pass without this subview.
+ scoped_nsobject<NSView> view(
+ [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 10, 10)]);
+ [[controller_ sidebarView] addSubview:view];
+
+ EXPECT_FALSE([[controller_ tabContentArea] isOpaque]);
+ EXPECT_FALSE([[controller_ devToolsView] isOpaque]);
+ EXPECT_FALSE([[controller_ sidebarView] isOpaque]);
+}
+
+// Tests that status bubble's base frame does move when devTools are docked.
+TEST_F(BrowserWindowControllerTest, TestStatusBubblePositioning) {
+ ASSERT_EQ(1U, [[[controller_ devToolsView] subviews] count]);
+
+ NSPoint bubbleOrigin = [controller_ statusBubbleBaseFrame].origin;
+
+ // Add a fake subview to devToolsView to emulate docked devTools.
+ scoped_nsobject<NSView> view(
+ [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 10, 10)]);
+ [[controller_ devToolsView] addSubview:view];
+ [[controller_ devToolsView] adjustSubviews];
+
+ NSPoint bubbleOriginWithDevTools = [controller_ statusBubbleBaseFrame].origin;
+
+ // Make sure that status bubble frame is moved.
+ EXPECT_FALSE(NSEqualPoints(bubbleOrigin, bubbleOriginWithDevTools));
+}
+
+@interface BrowserWindowControllerFakeFullscreen : BrowserWindowController {
+ @private
+ // We release the window ourselves, so we don't have to rely on the unittest
+ // doing it for us.
+ scoped_nsobject<NSWindow> fullscreenWindow_;
+}
+@end
+
+class BrowserWindowFullScreenControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ Browser* browser = browser_helper_.browser();
+ controller_ =
+ [[BrowserWindowControllerFakeFullscreen alloc] initWithBrowser:browser
+ takeOwnership:NO];
+ }
+
+ virtual void TearDown() {
+ [controller_ close];
+ CocoaTest::TearDown();
+ }
+
+ public:
+ BrowserTestHelper browser_helper_;
+ BrowserWindowController* controller_;
+};
+
+@interface BrowserWindowController (PrivateAPI)
+- (BOOL)supportsFullscreen;
+@end
+
+TEST_F(BrowserWindowFullScreenControllerTest, TestFullscreen) {
+ EXPECT_FALSE([controller_ isFullscreen]);
+ [controller_ setFullscreen:YES];
+ EXPECT_TRUE([controller_ isFullscreen]);
+ [controller_ setFullscreen:NO];
+ EXPECT_FALSE([controller_ isFullscreen]);
+}
+
+// If this test fails, it is usually a sign that the bots have some sort of
+// problem (such as a modal dialog up). This tests is a very useful canary, so
+// please do not mark it as flaky without first verifying that there are no bot
+// problems.
+TEST_F(BrowserWindowFullScreenControllerTest, TestActivate) {
+ EXPECT_FALSE([controller_ isFullscreen]);
+
+ [controller_ activate];
+ NSWindow* frontmostWindow = [[NSApp orderedWindows] objectAtIndex:0];
+ EXPECT_EQ(frontmostWindow, [controller_ window]);
+
+ [controller_ setFullscreen:YES];
+ [controller_ activate];
+ frontmostWindow = [[NSApp orderedWindows] objectAtIndex:0];
+ EXPECT_EQ(frontmostWindow, [controller_ createFullscreenWindow]);
+
+ // We have to cleanup after ourselves by unfullscreening.
+ [controller_ setFullscreen:NO];
+}
+
+@implementation BrowserWindowControllerFakeFullscreen
+// Override |-createFullscreenWindow| to return a dummy window. This isn't
+// needed to pass the test, but because the dummy window is only 100x100, it
+// prevents the real fullscreen window from flashing up and taking over the
+// whole screen. We have to return an actual window because |-layoutSubviews|
+// looks at the window's frame.
+- (NSWindow*)createFullscreenWindow {
+ if (fullscreenWindow_.get())
+ return fullscreenWindow_.get();
+
+ fullscreenWindow_.reset(
+ [[NSWindow alloc] initWithContentRect:NSMakeRect(0,0,400,400)
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:NO]);
+ return fullscreenWindow_.get();
+}
+@end
+
+/* TODO(???): test other methods of BrowserWindowController */
diff --git a/chrome/browser/ui/cocoa/browser_window_factory.mm b/chrome/browser/ui/cocoa/browser_window_factory.mm
new file mode 100644
index 0000000..e7222e7
--- /dev/null
+++ b/chrome/browser/ui/cocoa/browser_window_factory.mm
@@ -0,0 +1,32 @@
+// 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/basictypes.h"
+#include "chrome/browser/ui/cocoa/browser_window_cocoa.h"
+#include "chrome/browser/ui/cocoa/browser_window_controller.h"
+#include "chrome/browser/ui/cocoa/find_bar_bridge.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_window.h"
+
+// Create the controller for the Browser, which handles loading the browser
+// window from the nib. The controller takes ownership of |browser|.
+// static
+BrowserWindow* BrowserWindow::CreateBrowserWindow(Browser* browser) {
+ BrowserWindowController* controller =
+ [[BrowserWindowController alloc] initWithBrowser:browser];
+ return [controller browserWindow];
+}
+
+// static
+FindBar* BrowserWindow::CreateFindBar(Browser* browser) {
+ // We could push the AddFindBar() call into the FindBarBridge
+ // constructor or the FindBarCocoaController init, but that makes
+ // unit testing difficult, since we would also require a
+ // BrowserWindow object.
+ BrowserWindowCocoa* window =
+ static_cast<BrowserWindowCocoa*>(browser->window());
+ FindBarBridge* bridge = new FindBarBridge();
+ window->AddFindBar(bridge->find_bar_cocoa_controller());
+ return bridge;
+}
diff --git a/chrome/browser/ui/cocoa/bubble_view.h b/chrome/browser/ui/cocoa/bubble_view.h
new file mode 100644
index 0000000..755c8a4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bubble_view.h
@@ -0,0 +1,66 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+
+// A view class that looks like a "bubble" with rounded corners and displays
+// text inside. Can be themed. To put flush against the sides of a window, the
+// corner flags can be adjusted.
+
+// Constants that define where the bubble will have a rounded corner. If
+// not set, the corner will be square.
+enum {
+ kRoundedTopLeftCorner = 1,
+ kRoundedTopRightCorner = 1 << 1,
+ kRoundedBottomLeftCorner = 1 << 2,
+ kRoundedBottomRightCorner = 1 << 3,
+ kRoundedAllCorners = kRoundedTopLeftCorner & kRoundedTopRightCorner &
+ kRoundedBottomLeftCorner & kRoundedBottomRightCorner
+};
+
+// Constants that affect where the text is positioned within the view. They
+// are exposed in case anyone needs to use the padding to set the content string
+// length appropriately based on available space (such as eliding a URL).
+enum {
+ kBubbleViewTextPositionX = 4,
+ kBubbleViewTextPositionY = 2
+};
+
+@interface BubbleView : NSView {
+ @private
+ scoped_nsobject<NSString> content_;
+ unsigned long cornerFlags_;
+ // The window from which we get the theme used to draw. In some cases,
+ // it might not be the window we're in. As a result, this may or may not
+ // directly own us, so it needs to be weak to prevent a cycle.
+ NSWindow* themeProvider_;
+}
+
+// Designated initializer. |provider| is the window from which we get the
+// current theme to draw text and backgrounds. If nil, the current window will
+// be checked. The caller needs to ensure |provider| can't go away as it will
+// not be retained. Defaults to all corners being rounded.
+- (id)initWithFrame:(NSRect)frame themeProvider:(NSWindow*)provider;
+
+// Sets the string displayed in the bubble. A copy of the string is made.
+- (void)setContent:(NSString*)content;
+
+// Sets which corners will be rounded.
+- (void)setCornerFlags:(unsigned long)flags;
+
+// Sets the window whose theme is used to draw.
+- (void)setThemeProvider:(NSWindow*)provider;
+
+// The font used to display the content string.
+- (NSFont*)font;
+
+@end
+
+// APIs exposed only for testing.
+@interface BubbleView(TestingOnly)
+- (NSString*)content;
+- (unsigned long)cornerFlags;
+@end
diff --git a/chrome/browser/ui/cocoa/bubble_view.mm b/chrome/browser/ui/cocoa/bubble_view.mm
new file mode 100644
index 0000000..a888ebc
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bubble_view.mm
@@ -0,0 +1,120 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/bubble_view.h"
+
+#include "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h"
+#import "third_party/GTM/AppKit/GTMNSColor+Luminance.h"
+
+// The roundedness of the edges of our bubble.
+const int kBubbleCornerRadius = 4.0f;
+const float kWindowEdge = 0.7f;
+
+@implementation BubbleView
+
+// Designated initializer. |provider| is the window from which we get the
+// current theme to draw text and backgrounds. If nil, the current window will
+// be checked. The caller needs to ensure |provider| can't go away as it will
+// not be retained. Defaults to all corners being rounded.
+- (id)initWithFrame:(NSRect)frame themeProvider:(NSWindow*)provider {
+ if ((self = [super initWithFrame:frame])) {
+ cornerFlags_ = kRoundedAllCorners;
+ themeProvider_ = provider;
+ }
+ return self;
+}
+
+// Sets the string displayed in the bubble. A copy of the string is made.
+- (void)setContent:(NSString*)content {
+ if ([content_ isEqualToString:content])
+ return;
+ content_.reset([content copy]);
+ [self setNeedsDisplay:YES];
+}
+
+// Sets which corners will be rounded.
+- (void)setCornerFlags:(unsigned long)flags {
+ if (cornerFlags_ == flags)
+ return;
+ cornerFlags_ = flags;
+ [self setNeedsDisplay:YES];
+}
+
+- (void)setThemeProvider:(NSWindow*)provider {
+ if (themeProvider_ == provider)
+ return;
+ themeProvider_ = provider;
+ [self setNeedsDisplay:YES];
+}
+
+- (NSString*)content {
+ return content_.get();
+}
+
+- (unsigned long)cornerFlags {
+ return cornerFlags_;
+}
+
+// The font used to display the content string.
+- (NSFont*)font {
+ return [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
+}
+
+// Draws the themed background and the text. Will draw a gray bg if no theme.
+- (void)drawRect:(NSRect)rect {
+ float topLeftRadius =
+ cornerFlags_ & kRoundedTopLeftCorner ? kBubbleCornerRadius : 0;
+ float topRightRadius =
+ cornerFlags_ & kRoundedTopRightCorner ? kBubbleCornerRadius : 0;
+ float bottomLeftRadius =
+ cornerFlags_ & kRoundedBottomLeftCorner ? kBubbleCornerRadius : 0;
+ float bottomRightRadius =
+ cornerFlags_ & kRoundedBottomRightCorner ? kBubbleCornerRadius : 0;
+
+ ThemeProvider* themeProvider =
+ themeProvider_ ? [themeProvider_ themeProvider] :
+ [[self window] themeProvider];
+
+ // Background / Edge
+
+ NSRect bounds = [self bounds];
+ bounds = NSInsetRect(bounds, 0.5, 0.5);
+ NSBezierPath* border =
+ [NSBezierPath gtm_bezierPathWithRoundRect:bounds
+ topLeftCornerRadius:topLeftRadius
+ topRightCornerRadius:topRightRadius
+ bottomLeftCornerRadius:bottomLeftRadius
+ bottomRightCornerRadius:bottomRightRadius];
+
+ if (themeProvider)
+ [themeProvider->GetNSColor(BrowserThemeProvider::COLOR_TOOLBAR, true) set];
+ [border fill];
+
+ [[NSColor colorWithDeviceWhite:kWindowEdge alpha:1.0f] set];
+ [border stroke];
+
+ // Text
+ NSColor* textColor = [NSColor blackColor];
+ if (themeProvider)
+ textColor = themeProvider->GetNSColor(BrowserThemeProvider::COLOR_TAB_TEXT,
+ true);
+ NSFont* textFont = [self font];
+ scoped_nsobject<NSShadow> textShadow([[NSShadow alloc] init]);
+ [textShadow setShadowBlurRadius:0.0f];
+ [textShadow.get() setShadowColor:[textColor gtm_legibleTextColor]];
+ [textShadow.get() setShadowOffset:NSMakeSize(0.0f, -1.0f)];
+
+ NSDictionary* textDict = [NSDictionary dictionaryWithObjectsAndKeys:
+ textColor, NSForegroundColorAttributeName,
+ textFont, NSFontAttributeName,
+ textShadow.get(), NSShadowAttributeName,
+ nil];
+ [content_ drawAtPoint:NSMakePoint(kBubbleViewTextPositionX,
+ kBubbleViewTextPositionY)
+ withAttributes:textDict];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/bubble_view_unittest.mm b/chrome/browser/ui/cocoa/bubble_view_unittest.mm
new file mode 100644
index 0000000..5d788ea
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bubble_view_unittest.mm
@@ -0,0 +1,58 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/bubble_view.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "testing/gtest_mac.h"
+
+class BubbleViewTest : public CocoaTest {
+ public:
+ BubbleViewTest() {
+ NSRect frame = NSMakeRect(0, 0, 50, 50);
+ scoped_nsobject<BubbleView> view(
+ [[BubbleView alloc] initWithFrame:frame themeProvider:test_window()]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ [view_ setContent:@"Hi there, I'm a bubble view"];
+ }
+
+ BubbleView* view_;
+};
+
+TEST_VIEW(BubbleViewTest, view_);
+
+// Test a nil themeProvider in init.
+TEST_F(BubbleViewTest, NilThemeProvider) {
+ NSRect frame = NSMakeRect(0, 0, 50, 50);
+ scoped_nsobject<BubbleView> view(
+ [[BubbleView alloc] initWithFrame:frame themeProvider:nil]);
+ [[test_window() contentView] addSubview:view.get()];
+ [view display];
+}
+
+// Make sure things don't go haywire when given invalid or long strings.
+TEST_F(BubbleViewTest, SetContent) {
+ [view_ setContent:nil];
+ EXPECT_TRUE([view_ content] == nil);
+ [view_ setContent:@""];
+ EXPECT_NSEQ(@"", [view_ content]);
+ NSString* str = @"This is a really really long string that's just too long";
+ [view_ setContent:str];
+ EXPECT_NSEQ(str, [view_ content]);
+}
+
+TEST_F(BubbleViewTest, CornerFlags) {
+ // Set some random flags just to check.
+ [view_ setCornerFlags:kRoundedTopRightCorner | kRoundedTopLeftCorner];
+ EXPECT_EQ([view_ cornerFlags],
+ (unsigned long)kRoundedTopRightCorner | kRoundedTopLeftCorner);
+ // Set no flags (all 4 draw corners are square).
+ [view_ setCornerFlags:0];
+ EXPECT_EQ([view_ cornerFlags], 0UL);
+ // Set all bits. Meaningless past the first 4, but harmless to set too many.
+ [view_ setCornerFlags:0xFFFFFFFF];
+ EXPECT_EQ([view_ cornerFlags], 0xFFFFFFFF);
+}
diff --git a/chrome/browser/ui/cocoa/bug_report_window_controller.h b/chrome/browser/ui/cocoa/bug_report_window_controller.h
new file mode 100644
index 0000000..15aa707
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bug_report_window_controller.h
@@ -0,0 +1,112 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_BUG_REPORT_WINDOW_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_BUG_REPORT_WINDOW_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include <vector>
+
+#include "base/scoped_nsobject.h"
+
+class Profile;
+class TabContents;
+
+// A window controller for managing the "Report Bug" feature. Modally
+// presents a dialog that allows the user to either file a bug report on
+// a broken page, or go directly to Google's "Report Phishing" page and
+// file a report there.
+@interface BugReportWindowController : NSWindowController {
+ @private
+ TabContents* currentTab_; // Weak, owned by browser.
+ Profile* profile_; // Weak, owned by browser.
+
+ // Holds screenshot of current tab.
+ std::vector<unsigned char> pngData_;
+ // Width and height of the current tab's screenshot.
+ int pngWidth_;
+ int pngHeight_;
+
+ // Values bound to data in the dialog box. These values cannot be boxed in
+ // scoped_nsobjects because we use them for bindings.
+ NSString* bugDescription_; // Strong.
+ NSUInteger bugTypeIndex_;
+ NSString* pageTitle_; // Strong.
+ NSString* pageURL_; // Strong.
+
+ // We keep a pointer to this button so we can change its title.
+ IBOutlet NSButton* sendReportButton_;
+
+ // This button must be moved when the send report button changes title.
+ IBOutlet NSButton* cancelButton_;
+
+ // The popup button that allows choice of bug type.
+ IBOutlet NSPopUpButton* bugTypePopUpButton_;
+
+ // YES sends a screenshot along with the bug report.
+ BOOL sendScreenshot_;
+
+ // Disable screenshot if no browser window is open.
+ BOOL disableScreenshotCheckbox_;
+
+ // Menu for the bug type popup button. We create it here instead of in
+ // IB so that we can nicely check whether the phishing page is selected,
+ // and so that we can create a menu without "page" options when no browser
+ // window is open.
+ NSMutableArray* bugTypeList_; // Strong.
+
+ // When dialog switches from regular bug reports to phishing page, "save
+ // screenshot" and "description" are disabled. Save the state of this value
+ // to restore if the user switches back to a regular bug report before
+ // sending.
+ BOOL saveSendScreenshot_;
+ scoped_nsobject<NSString> saveBugDescription_; // Strong
+
+ // Maps bug type menu item title strings to BugReportUtil::BugType ints.
+ NSDictionary* bugTypeDictionary_; // Strong
+}
+
+// Initialize with the contents of the tab to be reported as buggy / wrong.
+// If dialog is called without an open window, currentTab may be null; in
+// that case, a dialog is opened with options for reporting a bugs not
+// related to a specific page. Profile is passed to BugReportUtil, who
+// will not send a report if the value is null.
+- (id)initWithTabContents:(TabContents*)currentTab profile:(Profile*)profile;
+
+// Run the dialog with an application-modal event loop. If the user accepts,
+// send the report of the bug or broken web site.
+- (void)runModalDialog;
+
+// IBActions for the dialog buttons.
+- (IBAction)sendReport:(id)sender;
+- (IBAction)cancel:(id)sender;
+
+// YES if the user has selected the phishing report option.
+- (BOOL)isPhishingReport;
+
+// Converts the bug type from the menu into the correct value for the bug type
+// from BugReportUtil::BugType.
+- (int)bugTypeFromIndex;
+
+// Force the description text field to allow "return" to go to the next line
+// within the description field. Without this delegate method, "return" falls
+// back to the "Send Report" action, because this button has been bound to
+// the return key in IB.
+- (BOOL)control:(NSControl*)control textView:(NSTextView*)textView
+ doCommandBySelector:(SEL)commandSelector;
+
+// Properties for bindings.
+@property (nonatomic, copy) NSString* bugDescription;
+@property (nonatomic) NSUInteger bugTypeIndex;
+@property (nonatomic, copy) NSString* pageTitle;
+@property (nonatomic, copy) NSString* pageURL;
+@property (nonatomic) BOOL sendScreenshot;
+@property (nonatomic) BOOL disableScreenshotCheckbox;
+@property (nonatomic, readonly) NSArray* bugTypeList;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_BUG_REPORT_WINDOW_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/bug_report_window_controller.mm b/chrome/browser/ui/cocoa/bug_report_window_controller.mm
new file mode 100644
index 0000000..82b3a36
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bug_report_window_controller.mm
@@ -0,0 +1,231 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/bug_report_window_controller.h"
+
+#include "app/l10n_util_mac.h"
+#include "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/bug_report_util.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents/tab_contents_view.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+@implementation BugReportWindowController
+
+@synthesize bugDescription = bugDescription_;
+@synthesize bugTypeIndex = bugTypeIndex_;
+@synthesize pageURL = pageURL_;
+@synthesize pageTitle = pageTitle_;
+@synthesize sendScreenshot = sendScreenshot_;
+@synthesize disableScreenshotCheckbox = disableScreenshotCheckbox_;
+@synthesize bugTypeList = bugTypeList_;
+
+- (id)initWithTabContents:(TabContents*)currentTab
+ profile:(Profile*)profile {
+ NSString* nibpath = [mac_util::MainAppBundle() pathForResource:@"ReportBug"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ currentTab_ = currentTab;
+ profile_ = profile;
+
+ // The order of strings in this array must match the order of the bug types
+ // declared below in the bugTypeFromIndex function.
+ bugTypeList_ = [[NSMutableArray alloc] initWithObjects:
+ l10n_util::GetNSStringWithFixup(IDS_BUGREPORT_CHROME_MISBEHAVES),
+ l10n_util::GetNSStringWithFixup(IDS_BUGREPORT_SOMETHING_MISSING),
+ l10n_util::GetNSStringWithFixup(IDS_BUGREPORT_BROWSER_CRASH),
+ l10n_util::GetNSStringWithFixup(IDS_BUGREPORT_OTHER_PROBLEM),
+ nil];
+
+ if (currentTab_ != NULL) {
+ // Get data from current tab, if one exists. This dialog could be called
+ // from the main menu with no tab contents, so currentTab_ is not
+ // guaranteed to be non-NULL.
+ // TODO(mirandac): This dialog should be a tab-modal sheet if a browser
+ // window exists.
+ [self setSendScreenshot:YES];
+ [self setDisableScreenshotCheckbox:NO];
+ // Insert menu items about bugs related to specific pages.
+ [bugTypeList_ insertObjects:
+ [NSArray arrayWithObjects:
+ l10n_util::GetNSStringWithFixup(IDS_BUGREPORT_PAGE_WONT_LOAD),
+ l10n_util::GetNSStringWithFixup(IDS_BUGREPORT_PAGE_LOOKS_ODD),
+ l10n_util::GetNSStringWithFixup(IDS_BUGREPORT_PHISHING_PAGE),
+ l10n_util::GetNSStringWithFixup(IDS_BUGREPORT_CANT_SIGN_IN),
+ nil]
+ atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 4)]];
+
+ [self setPageURL:base::SysUTF8ToNSString(
+ currentTab_->controller().GetActiveEntry()->url().spec())];
+ [self setPageTitle:base::SysUTF16ToNSString(currentTab_->GetTitle())];
+ mac_util::GrabWindowSnapshot(
+ currentTab_->view()->GetTopLevelNativeWindow(), &pngData_,
+ &pngWidth_, &pngHeight_);
+ } else {
+ // If no current tab exists, create a menu without the "broken page"
+ // options, with page URL and title empty, and screenshot disabled.
+ [self setSendScreenshot:NO];
+ [self setDisableScreenshotCheckbox:YES];
+ }
+
+ pngHeight_ = 0;
+ pngWidth_ = 0;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [pageURL_ release];
+ [pageTitle_ release];
+ [bugDescription_ release];
+ [bugTypeList_ release];
+ [bugTypeDictionary_ release];
+ [super dealloc];
+}
+
+// Delegate callback so that closing the window deletes the controller.
+- (void)windowWillClose:(NSNotification*)notification {
+ [self autorelease];
+}
+
+- (void)closeDialog {
+ [NSApp stopModal];
+ [[self window] close];
+}
+
+- (void)runModalDialog {
+ NSWindow* bugReportWindow = [self window];
+ [bugReportWindow center];
+ [NSApp runModalForWindow:bugReportWindow];
+}
+
+- (IBAction)sendReport:(id)sender {
+ if ([self isPhishingReport]) {
+ BugReportUtil::ReportPhishing(currentTab_,
+ pageURL_ ? base::SysNSStringToUTF8(pageURL_) : "");
+ } else {
+ BugReportUtil::SendReport(
+ profile_,
+ base::SysNSStringToUTF8(pageTitle_),
+ [self bugTypeFromIndex],
+ base::SysNSStringToUTF8(pageURL_),
+ base::SysNSStringToUTF8(bugDescription_),
+ sendScreenshot_ && !pngData_.empty() ?
+ reinterpret_cast<const char *>(&(pngData_[0])) : NULL,
+ pngData_.size(), pngWidth_, pngHeight_);
+ }
+ [self closeDialog];
+}
+
+- (IBAction)cancel:(id)sender {
+ [self closeDialog];
+}
+
+- (BOOL)isPhishingReport {
+ return [self bugTypeFromIndex] == BugReportUtil::PHISHING_PAGE;
+}
+
+- (int)bugTypeFromIndex {
+ // The order of these bugs must match the ordering in the bugTypeList_,
+ // and thereby the menu in the popup button in the dialog box.
+ const BugReportUtil::BugType typesForMenuIndices[] = {
+ BugReportUtil::PAGE_WONT_LOAD,
+ BugReportUtil::PAGE_LOOKS_ODD,
+ BugReportUtil::PHISHING_PAGE,
+ BugReportUtil::CANT_SIGN_IN,
+ BugReportUtil::CHROME_MISBEHAVES,
+ BugReportUtil::SOMETHING_MISSING,
+ BugReportUtil::BROWSER_CRASH,
+ BugReportUtil::OTHER_PROBLEM
+ };
+ // The bugs for the shorter menu start at index 4.
+ NSUInteger adjustedBugTypeIndex_ = [bugTypeList_ count] == 8 ? bugTypeIndex_ :
+ bugTypeIndex_ + 4;
+ DCHECK_LT(adjustedBugTypeIndex_, arraysize(typesForMenuIndices));
+ return typesForMenuIndices[adjustedBugTypeIndex_];
+}
+
+// Custom setter to update the UI for different bug types.
+- (void)setBugTypeIndex:(NSUInteger)bugTypeIndex {
+ bugTypeIndex_ = bugTypeIndex;
+
+ // The "send" button's title is based on the type of report.
+ NSString* buttonTitle = [self isPhishingReport] ?
+ l10n_util::GetNSStringWithFixup(IDS_BUGREPORT_SEND_PHISHING_REPORT) :
+ l10n_util::GetNSStringWithFixup(IDS_BUGREPORT_SEND_REPORT);
+ if (![buttonTitle isEqualTo:[sendReportButton_ title]]) {
+ NSRect sendFrame1 = [sendReportButton_ frame];
+ NSRect cancelFrame1 = [cancelButton_ frame];
+
+ [sendReportButton_ setTitle:buttonTitle];
+ CGFloat deltaWidth =
+ [GTMUILocalizerAndLayoutTweaker sizeToFitView:sendReportButton_].width;
+
+ NSRect sendFrame2 = [sendReportButton_ frame];
+ sendFrame2.origin.x -= deltaWidth;
+ NSRect cancelFrame2 = cancelFrame1;
+ cancelFrame2.origin.x -= deltaWidth;
+
+ // Since the buttons get updated/resize, use a quick animation so it is
+ // a little less jarring in the UI.
+ NSDictionary* sendReportButtonResize =
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ sendReportButton_, NSViewAnimationTargetKey,
+ [NSValue valueWithRect:sendFrame1], NSViewAnimationStartFrameKey,
+ [NSValue valueWithRect:sendFrame2], NSViewAnimationEndFrameKey,
+ nil];
+ NSDictionary* cancelButtonResize =
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ cancelButton_, NSViewAnimationTargetKey,
+ [NSValue valueWithRect:cancelFrame1], NSViewAnimationStartFrameKey,
+ [NSValue valueWithRect:cancelFrame2], NSViewAnimationEndFrameKey,
+ nil];
+ NSAnimation* animation =
+ [[[NSViewAnimation alloc] initWithViewAnimations:
+ [NSArray arrayWithObjects:sendReportButtonResize, cancelButtonResize,
+ nil]] autorelease];
+ const NSTimeInterval kQuickTransitionInterval = 0.1;
+ [animation setDuration:kQuickTransitionInterval];
+ [animation startAnimation];
+
+ // Save or reload description when moving between phishing page and other
+ // bug report types.
+ if ([self isPhishingReport]) {
+ saveBugDescription_.reset([[self bugDescription] retain]);
+ [self setBugDescription:nil];
+ saveSendScreenshot_ = sendScreenshot_;
+ [self setSendScreenshot:NO];
+ } else {
+ [self setBugDescription:saveBugDescription_.get()];
+ saveBugDescription_.reset();
+ [self setSendScreenshot:saveSendScreenshot_];
+ }
+ }
+}
+
+- (BOOL)control:(NSControl*)control textView:(NSTextView*)textView
+ doCommandBySelector:(SEL)commandSelector {
+ if (commandSelector == @selector(insertNewline:)) {
+ [textView insertNewlineIgnoringFieldEditor:self];
+ return YES;
+ }
+ return NO;
+}
+
+// BugReportWindowController needs to change the title of the Send Report
+// button when the user chooses the phishing bug type, so we need to bind
+// the function that changes the button title to the bug type key.
++ (NSSet*)keyPathsForValuesAffectingValueForKey:(NSString*)key {
+ NSSet* paths = [super keyPathsForValuesAffectingValueForKey:key];
+ if ([key isEqualToString:@"isPhishingReport"]) {
+ paths = [paths setByAddingObject:@"bugTypeIndex"];
+ }
+ return paths;
+}
+
+@end
+
diff --git a/chrome/browser/ui/cocoa/bug_report_window_controller_unittest.mm b/chrome/browser/ui/cocoa/bug_report_window_controller_unittest.mm
new file mode 100644
index 0000000..b95f251
--- /dev/null
+++ b/chrome/browser/ui/cocoa/bug_report_window_controller_unittest.mm
@@ -0,0 +1,78 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/ref_counted.h"
+#include "chrome/browser/browser_thread.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/renderer_host/site_instance.h"
+#include "chrome/browser/renderer_host/test/test_render_view_host.h"
+#include "chrome/browser/tab_contents/test_tab_contents.h"
+#import "chrome/browser/ui/cocoa/bug_report_window_controller.h"
+#include "chrome/test/testing_profile.h"
+#import "testing/gtest_mac.h"
+
+namespace {
+
+class BugReportWindowControllerUnittest : public RenderViewHostTestHarness {
+};
+
+// See http://crbug.com/29019 for why it's disabled.
+TEST_F(BugReportWindowControllerUnittest, DISABLED_ReportBugWithNewTabPageOpen) {
+ BrowserThread ui_thread(BrowserThread::UI, MessageLoop::current());
+ // Create a "chrome://newtab" test tab. SiteInstance will be deleted when
+ // tabContents is deleted.
+ SiteInstance* instance =
+ SiteInstance::CreateSiteInstance(profile_.get());
+ TestTabContents* tabContents = new TestTabContents(profile_.get(),
+ instance);
+ tabContents->controller().LoadURL(GURL("chrome://newtab"),
+ GURL(), PageTransition::START_PAGE);
+
+ BugReportWindowController* controller = [[BugReportWindowController alloc]
+ initWithTabContents:tabContents
+ profile:profile_.get()];
+
+ // The phishing report bug is stored at index 2 in the Report Bug dialog.
+ [controller setBugTypeIndex:2];
+ EXPECT_TRUE([controller isPhishingReport]);
+ [controller setBugTypeIndex:1];
+ EXPECT_FALSE([controller isPhishingReport]);
+
+ // Make sure that the tab was correctly recorded.
+ EXPECT_NSEQ(@"chrome://newtab/", [controller pageURL]);
+ EXPECT_NSEQ(@"New Tab", [controller pageTitle]);
+
+ // When we call "report bug" with non-empty tab contents, all menu options
+ // should be available, and we should send screenshot by default.
+ EXPECT_EQ([[controller bugTypeList] count], 8U);
+ EXPECT_TRUE([controller sendScreenshot]);
+
+ delete tabContents;
+ [controller release];
+}
+
+// See http://crbug.com/29019 for why it's disabled.
+TEST_F(BugReportWindowControllerUnittest, DISABLED_ReportBugWithNoWindowOpen) {
+ BugReportWindowController* controller = [[BugReportWindowController alloc]
+ initWithTabContents:NULL
+ profile:profile_.get()];
+
+ // Make sure that no page title or URL are recorded. Note that IB reports
+ // empty textfields as NULL values.
+ EXPECT_FALSE([controller pageURL]);
+ EXPECT_FALSE([controller pageTitle]);
+
+ // When we call "report bug" with empty tab contents, only menu options
+ // that don't refer to a specific page should be available, and the send
+ // screenshot option should be turned off.
+ EXPECT_EQ([[controller bugTypeList] count], 4U);
+ EXPECT_FALSE([controller sendScreenshot]);
+
+ [controller release];
+}
+
+} // namespace
+
diff --git a/chrome/browser/ui/cocoa/certificate_viewer.mm b/chrome/browser/ui/cocoa/certificate_viewer.mm
new file mode 100644
index 0000000..8c5a954
--- /dev/null
+++ b/chrome/browser/ui/cocoa/certificate_viewer.mm
@@ -0,0 +1,45 @@
+// 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 "chrome/browser/certificate_viewer.h"
+
+#include <Security/Security.h>
+#include <SecurityInterface/SFCertificatePanel.h>
+
+#include <vector>
+
+#include "base/logging.h"
+#include "base/mac/scoped_cftyperef.h"
+#include "net/base/x509_certificate.h"
+
+void ShowCertificateViewer(gfx::NativeWindow parent,
+ net::X509Certificate* cert) {
+ SecCertificateRef cert_mac = cert->os_cert_handle();
+ if (!cert_mac)
+ return;
+
+ base::mac::ScopedCFTypeRef<CFMutableArrayRef> certificates(
+ CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks));
+ if (!certificates.get()) {
+ NOTREACHED();
+ return;
+ }
+ CFArrayAppendValue(certificates, cert_mac);
+
+ // Server certificate must be first in the array; subsequent certificates
+ // in the chain can be in any order.
+ const std::vector<SecCertificateRef>& ca_certs =
+ cert->GetIntermediateCertificates();
+ for (size_t i = 0; i < ca_certs.size(); ++i)
+ CFArrayAppendValue(certificates, ca_certs[i]);
+
+ [[[SFCertificatePanel alloc] init]
+ beginSheetForWindow:parent
+ modalDelegate:nil
+ didEndSelector:NULL
+ contextInfo:NULL
+ certificates:reinterpret_cast<NSArray*>(certificates.get())
+ showGroup:YES];
+ // The SFCertificatePanel releases itself when the sheet is dismissed.
+}
diff --git a/chrome/browser/ui/cocoa/chrome_browser_window.h b/chrome/browser/ui/cocoa/chrome_browser_window.h
new file mode 100644
index 0000000..64c0123
--- /dev/null
+++ b/chrome/browser/ui/cocoa/chrome_browser_window.h
@@ -0,0 +1,28 @@
+// 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_UI_COCOA_CHROME_BROWSER_WINDOW_H_
+#define CHROME_BROWSER_UI_COCOA_CHROME_BROWSER_WINDOW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/ui/cocoa/chrome_event_processing_window.h"
+
+// Common base class for chrome browser windows. Contains methods relating to
+// theming and hole punching that are shared between framed and fullscreen
+// windows.
+@interface ChromeBrowserWindow : ChromeEventProcessingWindow {
+ @private
+ int underlaySurfaceCount_;
+}
+
+// Informs the window that an underlay surface has been added/removed. The
+// window is non-opaque while underlay surfaces are present.
+- (void)underlaySurfaceAdded;
+- (void)underlaySurfaceRemoved;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_CHROME_BROWSER_WINDOW_H_
diff --git a/chrome/browser/ui/cocoa/chrome_browser_window.mm b/chrome/browser/ui/cocoa/chrome_browser_window.mm
new file mode 100644
index 0000000..abac221
--- /dev/null
+++ b/chrome/browser/ui/cocoa/chrome_browser_window.mm
@@ -0,0 +1,52 @@
+// 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/ui/cocoa/chrome_browser_window.h"
+
+#include "base/logging.h"
+#include "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+
+@implementation ChromeBrowserWindow
+
+- (void)underlaySurfaceAdded {
+ DCHECK_GE(underlaySurfaceCount_, 0);
+ ++underlaySurfaceCount_;
+
+ // We're having the OpenGL surface render under the window, so the window
+ // needs to be not opaque.
+ if (underlaySurfaceCount_ == 1)
+ [self setOpaque:NO];
+}
+
+- (void)underlaySurfaceRemoved {
+ --underlaySurfaceCount_;
+ DCHECK_GE(underlaySurfaceCount_, 0);
+
+ if (underlaySurfaceCount_ == 0)
+ [self setOpaque:YES];
+}
+
+- (ThemeProvider*)themeProvider {
+ id delegate = [self delegate];
+ if (![delegate respondsToSelector:@selector(themeProvider)])
+ return NULL;
+ return [delegate themeProvider];
+}
+
+- (ThemedWindowStyle)themedWindowStyle {
+ id delegate = [self delegate];
+ if (![delegate respondsToSelector:@selector(themedWindowStyle)])
+ return THEMED_NORMAL;
+ return [delegate themedWindowStyle];
+}
+
+- (NSPoint)themePatternPhase {
+ id delegate = [self delegate];
+ if (![delegate respondsToSelector:@selector(themePatternPhase)])
+ return NSMakePoint(0, 0);
+ return [delegate themePatternPhase];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/chrome_browser_window_unittest.mm b/chrome/browser/ui/cocoa/chrome_browser_window_unittest.mm
new file mode 100644
index 0000000..196dc74
--- /dev/null
+++ b/chrome/browser/ui/cocoa/chrome_browser_window_unittest.mm
@@ -0,0 +1,45 @@
+// 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/debug/debugger.h"
+#import "chrome/browser/ui/cocoa/chrome_browser_window.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+class ChromeBrowserWindowTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ // Create a window.
+ const NSUInteger mask = NSTitledWindowMask | NSClosableWindowMask |
+ NSMiniaturizableWindowMask | NSResizableWindowMask;
+ window_ = [[ChromeBrowserWindow alloc]
+ initWithContentRect:NSMakeRect(0, 0, 800, 600)
+ styleMask:mask
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ if (base::debug::BeingDebugged()) {
+ [window_ orderFront:nil];
+ } else {
+ [window_ orderBack:nil];
+ }
+ }
+
+ virtual void TearDown() {
+ [window_ close];
+ CocoaTest::TearDown();
+ }
+
+ ChromeBrowserWindow* window_;
+};
+
+// Baseline test that the window creates, displays, closes, and
+// releases.
+TEST_F(ChromeBrowserWindowTest, ShowAndClose) {
+ [window_ display];
+}
diff --git a/chrome/browser/ui/cocoa/chrome_event_processing_window.h b/chrome/browser/ui/cocoa/chrome_event_processing_window.h
new file mode 100644
index 0000000..3524d6f1a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/chrome_event_processing_window.h
@@ -0,0 +1,49 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_CHROME_EVENT_PROCESSING_WINDOW_H_
+#define CHROME_BROWSER_UI_COCOA_CHROME_EVENT_PROCESSING_WINDOW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+
+// Override NSWindow to access unhandled keyboard events (for command
+// processing); subclassing NSWindow is the only method to do
+// this.
+@interface ChromeEventProcessingWindow : NSWindow {
+ @private
+ BOOL redispatchingEvent_;
+ BOOL eventHandled_;
+}
+
+// Sends a key event to |NSApp sendEvent:|, but also makes sure that it's not
+// short-circuited to the RWHV. This is used to send keyboard events to the menu
+// and the cmd-` handler if a keyboard event comes back unhandled from the
+// renderer. The event must be of type |NSKeyDown|, |NSKeyUp|, or
+// |NSFlagsChanged|.
+// Returns |YES| if |event| has been handled.
+- (BOOL)redispatchKeyEvent:(NSEvent*)event;
+
+// See global_keyboard_shortcuts_mac.h for details on the next two functions.
+
+// Checks if |event| is a window keyboard shortcut. If so, dispatches it to the
+// window controller's |executeCommand:| and returns |YES|.
+- (BOOL)handleExtraWindowKeyboardShortcut:(NSEvent*)event;
+
+// Checks if |event| is a delayed window keyboard shortcut. If so, dispatches
+// it to the window controller's |executeCommand:| and returns |YES|.
+- (BOOL)handleDelayedWindowKeyboardShortcut:(NSEvent*)event;
+
+// Checks if |event| is a browser keyboard shortcut. If so, dispatches it to the
+// window controller's |executeCommand:| and returns |YES|.
+- (BOOL)handleExtraBrowserKeyboardShortcut:(NSEvent*)event;
+
+// Override, so we can handle global keyboard events.
+- (BOOL)performKeyEquivalent:(NSEvent*)theEvent;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_CHROME_EVENT_PROCESSING_WINDOW_H_
diff --git a/chrome/browser/ui/cocoa/chrome_event_processing_window.mm b/chrome/browser/ui/cocoa/chrome_event_processing_window.mm
new file mode 100644
index 0000000..be6591b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/chrome_event_processing_window.mm
@@ -0,0 +1,164 @@
+// 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/ui/cocoa/chrome_event_processing_window.h"
+
+#include "base/logging.h"
+#import "chrome/browser/renderer_host/render_widget_host_view_mac.h"
+#import "chrome/browser/ui/cocoa/browser_command_executor.h"
+#import "chrome/browser/ui/cocoa/browser_frame_view.h"
+#import "chrome/browser/ui/cocoa/tab_strip_controller.h"
+#include "chrome/browser/global_keyboard_shortcuts_mac.h"
+
+typedef int (*KeyToCommandMapper)(bool, bool, bool, bool, int, unichar);
+
+@interface ChromeEventProcessingWindow ()
+// Duplicate the given key event, but changing the associated window.
+- (NSEvent*)keyEventForWindow:(NSWindow*)window fromKeyEvent:(NSEvent*)event;
+@end
+
+@implementation ChromeEventProcessingWindow
+
+- (BOOL)handleExtraKeyboardShortcut:(NSEvent*)event fromTable:
+ (KeyToCommandMapper)commandForKeyboardShortcut {
+ // Extract info from |event|.
+ NSUInteger modifers = [event modifierFlags];
+ const bool cmdKey = modifers & NSCommandKeyMask;
+ const bool shiftKey = modifers & NSShiftKeyMask;
+ const bool cntrlKey = modifers & NSControlKeyMask;
+ const bool optKey = modifers & NSAlternateKeyMask;
+ const unichar keyCode = [event keyCode];
+ const unichar keyChar = KeyCharacterForEvent(event);
+
+ int cmdNum = commandForKeyboardShortcut(cmdKey, shiftKey, cntrlKey, optKey,
+ keyCode, keyChar);
+
+ if (cmdNum != -1) {
+ id executor = [self delegate];
+ // A bit of sanity.
+ DCHECK([executor conformsToProtocol:@protocol(BrowserCommandExecutor)]);
+ DCHECK([executor respondsToSelector:@selector(executeCommand:)]);
+ [executor executeCommand:cmdNum];
+ return YES;
+ }
+ return NO;
+}
+
+- (BOOL)handleExtraWindowKeyboardShortcut:(NSEvent*)event {
+ return [self handleExtraKeyboardShortcut:event
+ fromTable:CommandForWindowKeyboardShortcut];
+}
+
+- (BOOL)handleDelayedWindowKeyboardShortcut:(NSEvent*)event {
+ return [self handleExtraKeyboardShortcut:event
+ fromTable:CommandForDelayedWindowKeyboardShortcut];
+}
+
+- (BOOL)handleExtraBrowserKeyboardShortcut:(NSEvent*)event {
+ return [self handleExtraKeyboardShortcut:event
+ fromTable:CommandForBrowserKeyboardShortcut];
+}
+
+- (BOOL)performKeyEquivalent:(NSEvent*)event {
+ if (redispatchingEvent_)
+ return NO;
+
+ // Give the web site a chance to handle the event. If it doesn't want to
+ // handle it, it will call us back with one of the |handle*| methods above.
+ NSResponder* r = [self firstResponder];
+ if ([r isKindOfClass:[RenderWidgetHostViewCocoa class]])
+ return [r performKeyEquivalent:event];
+
+ // If the delegate does not implement the BrowserCommandExecutor protocol,
+ // then we don't need to handle browser specific shortcut keys.
+ if (![[self delegate] conformsToProtocol:@protocol(BrowserCommandExecutor)])
+ return [super performKeyEquivalent:event];
+
+ // Handle per-window shortcuts like cmd-1, but do not handle browser-level
+ // shortcuts like cmd-left (else, cmd-left would do history navigation even
+ // if e.g. the Omnibox has focus).
+ if ([self handleExtraWindowKeyboardShortcut:event])
+ return YES;
+
+ if ([super performKeyEquivalent:event])
+ return YES;
+
+ // Handle per-window shortcuts like Esc after giving everybody else a chance
+ // to handle them
+ return [self handleDelayedWindowKeyboardShortcut:event];
+}
+
+- (BOOL)redispatchKeyEvent:(NSEvent*)event {
+ DCHECK(event);
+ NSEventType eventType = [event type];
+ if (eventType != NSKeyDown &&
+ eventType != NSKeyUp &&
+ eventType != NSFlagsChanged) {
+ NOTREACHED();
+ return YES; // Pretend it's been handled in an effort to limit damage.
+ }
+
+ // Ordinarily, the event's window should be this window. However, when
+ // switching between normal and fullscreen mode, we switch out the window, and
+ // the event's window might be the previous window (or even an earlier one if
+ // the renderer is running slowly and several mode switches occur). In this
+ // rare case, we synthesize a new key event so that its associate window
+ // (number) is our own.
+ if ([event window] != self)
+ event = [self keyEventForWindow:self fromKeyEvent:event];
+
+ // Redispatch the event.
+ eventHandled_ = YES;
+ redispatchingEvent_ = YES;
+ [NSApp sendEvent:event];
+ redispatchingEvent_ = NO;
+
+ // If the event was not handled by [NSApp sendEvent:], the sendEvent:
+ // method below will be called, and because |redispatchingEvent_| is YES,
+ // |eventHandled_| will be set to NO.
+ return eventHandled_;
+}
+
+- (void)sendEvent:(NSEvent*)event {
+ if (!redispatchingEvent_)
+ [super sendEvent:event];
+ else
+ eventHandled_ = NO;
+}
+
+- (NSEvent*)keyEventForWindow:(NSWindow*)window fromKeyEvent:(NSEvent*)event {
+ NSEventType eventType = [event type];
+
+ // Convert the event's location from the original window's coordinates into
+ // our own.
+ NSPoint eventLoc = [event locationInWindow];
+ eventLoc = [[event window] convertBaseToScreen:eventLoc];
+ eventLoc = [self convertScreenToBase:eventLoc];
+
+ // Various things *only* apply to key down/up.
+ BOOL eventIsARepeat = NO;
+ NSString* eventCharacters = nil;
+ NSString* eventUnmodCharacters = nil;
+ if (eventType == NSKeyDown || eventType == NSKeyUp) {
+ eventIsARepeat = [event isARepeat];
+ eventCharacters = [event characters];
+ eventUnmodCharacters = [event charactersIgnoringModifiers];
+ }
+
+ // This synthesis may be slightly imperfect: we provide nil for the context,
+ // since I (viettrungluu) am sceptical that putting in the original context
+ // (if one is given) is valid.
+ return [NSEvent keyEventWithType:eventType
+ location:eventLoc
+ modifierFlags:[event modifierFlags]
+ timestamp:[event timestamp]
+ windowNumber:[window windowNumber]
+ context:nil
+ characters:eventCharacters
+ charactersIgnoringModifiers:eventUnmodCharacters
+ isARepeat:eventIsARepeat
+ keyCode:[event keyCode]];
+}
+
+@end // ChromeEventProcessingWindow
diff --git a/chrome/browser/ui/cocoa/chrome_event_processing_window_unittest.mm b/chrome/browser/ui/cocoa/chrome_event_processing_window_unittest.mm
new file mode 100644
index 0000000..9cbb8e0
--- /dev/null
+++ b/chrome/browser/ui/cocoa/chrome_event_processing_window_unittest.mm
@@ -0,0 +1,104 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/debug/debugger.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/app/chrome_command_ids.h"
+#import "chrome/browser/ui/cocoa/chrome_event_processing_window.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/browser_frame_view.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+
+namespace {
+
+NSEvent* KeyEvent(const NSUInteger flags, const NSUInteger keyCode) {
+ return [NSEvent keyEventWithType:NSKeyDown
+ location:NSZeroPoint
+ modifierFlags:flags
+ timestamp:0.0
+ windowNumber:0
+ context:nil
+ characters:@""
+ charactersIgnoringModifiers:@""
+ isARepeat:NO
+ keyCode:keyCode];
+}
+
+class ChromeEventProcessingWindowTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ // Create a window.
+ const NSUInteger mask = NSTitledWindowMask | NSClosableWindowMask |
+ NSMiniaturizableWindowMask | NSResizableWindowMask;
+ window_ = [[ChromeEventProcessingWindow alloc]
+ initWithContentRect:NSMakeRect(0, 0, 800, 600)
+ styleMask:mask
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ if (base::debug::BeingDebugged()) {
+ [window_ orderFront:nil];
+ } else {
+ [window_ orderBack:nil];
+ }
+ }
+
+ virtual void TearDown() {
+ [window_ close];
+ CocoaTest::TearDown();
+ }
+
+ ChromeEventProcessingWindow* window_;
+};
+
+id CreateBrowserWindowControllerMock() {
+ id delegate = [OCMockObject mockForClass:[BrowserWindowController class]];
+ // Make conformsToProtocol return YES for @protocol(BrowserCommandExecutor)
+ // to satisfy the DCHECK() in handleExtraKeyboardShortcut.
+ //
+ // TODO(akalin): Figure out how to replace OCMOCK_ANY below with
+ // @protocol(BrowserCommandExecutor) and have it work.
+ BOOL yes = YES;
+ [[[delegate stub] andReturnValue:OCMOCK_VALUE(yes)]
+ conformsToProtocol:OCMOCK_ANY];
+ return delegate;
+}
+
+// Verify that the window intercepts a particular key event and
+// forwards it to [delegate executeCommand:]. Assume that other
+// CommandForKeyboardShortcut() will work the same for the rest.
+TEST_F(ChromeEventProcessingWindowTest,
+ PerformKeyEquivalentForwardToExecuteCommand) {
+ NSEvent* event = KeyEvent(NSCommandKeyMask, kVK_ANSI_1);
+
+ id delegate = CreateBrowserWindowControllerMock();
+ [[delegate expect] executeCommand:IDC_SELECT_TAB_0];
+
+ [window_ setDelegate:delegate];
+ [window_ performKeyEquivalent:event];
+
+ // Don't wish to mock all the way down...
+ [window_ setDelegate:nil];
+ [delegate verify];
+}
+
+// Verify that an unhandled shortcut does not get forwarded via
+// -executeCommand:.
+// TODO(shess) Think of a way to test that it is sent to the
+// superclass.
+TEST_F(ChromeEventProcessingWindowTest, PerformKeyEquivalentNoForward) {
+ NSEvent* event = KeyEvent(0, 0);
+
+ id delegate = CreateBrowserWindowControllerMock();
+
+ [window_ setDelegate:delegate];
+ [window_ performKeyEquivalent:event];
+
+ // Don't wish to mock all the way down...
+ [window_ setDelegate:nil];
+ [delegate verify];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/clear_browsing_data_controller.h b/chrome/browser/ui/cocoa/clear_browsing_data_controller.h
new file mode 100644
index 0000000..776841d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/clear_browsing_data_controller.h
@@ -0,0 +1,87 @@
+// 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_UI_COCOA_CLEAR_BROWSING_DATA_CONTROLLER_
+#define CHROME_BROWSER_UI_COCOA_CLEAR_BROWSING_DATA_CONTROLLER_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_ptr.h"
+
+class BrowsingDataRemover;
+class ClearBrowsingObserver;
+class Profile;
+@class ThrobberView;
+
+// Name of notification that is called when data is cleared.
+extern NSString* const kClearBrowsingDataControllerDidDelete;
+// A key in the above notification's userInfo. Contains a NSNumber with the
+// logically-ored constants defined in BrowsingDataRemover for the removal.
+extern NSString* const kClearBrowsingDataControllerRemoveMask;
+
+// A window controller for managing the "Clear Browsing Data" feature. Modally
+// presents a dialog offering the user a set of choices of what browsing data
+// to delete and does so if the user chooses.
+
+@interface ClearBrowsingDataController : NSWindowController {
+ @private
+ Profile* profile_; // Weak, owned by browser.
+ // If non-null means there is a removal in progress. Member used mainly for
+ // automated tests. The remove deletes itself when it's done, so this is a
+ // weak reference.
+ BrowsingDataRemover* remover_;
+ scoped_ptr<ClearBrowsingObserver> observer_;
+ BOOL isClearing_; // YES while clearing data is ongoing.
+
+ // Values for checkboxes, kept in sync with bindings. These values get
+ // persisted into prefs if the user accepts the dialog.
+ BOOL clearBrowsingHistory_;
+ BOOL clearDownloadHistory_;
+ BOOL emptyCache_;
+ BOOL deleteCookies_;
+ BOOL clearSavedPasswords_;
+ BOOL clearFormData_;
+ NSInteger timePeriod_;
+}
+
+// Show the clear browsing data window. Do not use |-initWithProfile:|,
+// go through this instead so we don't end up with multiple instances.
+// This function does not block, so it can be used from DOMUI calls.
++ (void)showClearBrowsingDialogForProfile:(Profile*)profile;
++ (ClearBrowsingDataController*)controllerForProfile:(Profile*)profile;
+
+// Run the dialog with an application-modal event loop. If the user accepts,
+// performs the deletion of the selected browsing data. The values of the
+// checkboxes will be persisted into prefs for next time.
+- (void)runModalDialog;
+
+// IBActions for the dialog buttons
+- (IBAction)clearData:(id)sender;
+- (IBAction)cancel:(id)sender;
+- (IBAction)openFlashPlayerSettings:(id)sender;
+
+// Properties for bindings
+@property (nonatomic) BOOL clearBrowsingHistory;
+@property (nonatomic) BOOL clearDownloadHistory;
+@property (nonatomic) BOOL emptyCache;
+@property (nonatomic) BOOL deleteCookies;
+@property (nonatomic) BOOL clearSavedPasswords;
+@property (nonatomic) BOOL clearFormData;
+@property (nonatomic) NSInteger timePeriod;
+@property (nonatomic) BOOL isClearing;
+
+@end
+
+
+@interface ClearBrowsingDataController (ExposedForUnitTests)
+// Create the controller with the given profile (which must not be NULL).
+- (id)initWithProfile:(Profile*)profile;
+@property (readonly) int removeMask;
+- (void)persistToPrefs;
+- (void)closeDialog;
+- (void)dataRemoverDidFinish;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_CLEAR_BROWSING_DATA_CONTROLLER_
diff --git a/chrome/browser/ui/cocoa/clear_browsing_data_controller.mm b/chrome/browser/ui/cocoa/clear_browsing_data_controller.mm
new file mode 100644
index 0000000..927c2e4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/clear_browsing_data_controller.mm
@@ -0,0 +1,264 @@
+// 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/ui/cocoa/clear_browsing_data_controller.h"
+
+#include "app/l10n_util.h"
+#include "base/mac_util.h"
+#include "base/scoped_nsobject.h"
+#include "base/singleton.h"
+#include "chrome/browser/browsing_data_remover.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chrome/common/pref_names.h"
+#include "grit/locale_settings.h"
+#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+NSString* const kClearBrowsingDataControllerDidDelete =
+ @"kClearBrowsingDataControllerDidDelete";
+NSString* const kClearBrowsingDataControllerRemoveMask =
+ @"kClearBrowsingDataControllerRemoveMask";
+
+@interface ClearBrowsingDataController(Private)
+- (void)initFromPrefs;
+- (void)persistToPrefs;
+- (void)dataRemoverDidFinish;
+@end
+
+class ClearBrowsingObserver : public BrowsingDataRemover::Observer {
+ public:
+ ClearBrowsingObserver(ClearBrowsingDataController* controller)
+ : controller_(controller) { }
+ void OnBrowsingDataRemoverDone() { [controller_ dataRemoverDidFinish]; }
+ private:
+ ClearBrowsingDataController* controller_;
+};
+
+namespace {
+
+typedef std::map<Profile*, ClearBrowsingDataController*> ProfileControllerMap;
+
+} // namespace
+
+@implementation ClearBrowsingDataController
+
+@synthesize clearBrowsingHistory = clearBrowsingHistory_;
+@synthesize clearDownloadHistory = clearDownloadHistory_;
+@synthesize emptyCache = emptyCache_;
+@synthesize deleteCookies = deleteCookies_;
+@synthesize clearSavedPasswords = clearSavedPasswords_;
+@synthesize clearFormData = clearFormData_;
+@synthesize timePeriod = timePeriod_;
+@synthesize isClearing = isClearing_;
+
++ (void)showClearBrowsingDialogForProfile:(Profile*)profile {
+ ClearBrowsingDataController* controller =
+ [ClearBrowsingDataController controllerForProfile:profile];
+ if (![controller isWindowLoaded]) {
+ // This function needs to return instead of blocking, to match the windows
+ // api call. It caused problems when launching the dialog from the
+ // DomUI history page. See bug and code review for more details.
+ // http://crbug.com/37976
+ [controller performSelector:@selector(runModalDialog)
+ withObject:nil
+ afterDelay:0];
+ }
+}
+
++ (ClearBrowsingDataController *)controllerForProfile:(Profile*)profile {
+ // Get the original profile in case we get here from an incognito window
+ // |GetOriginalProfile()| will return the same profile if it is the original
+ // profile.
+ profile = profile->GetOriginalProfile();
+
+ ProfileControllerMap* map = Singleton<ProfileControllerMap>::get();
+ DCHECK(map != NULL);
+ ProfileControllerMap::iterator it = map->find(profile);
+ if (it == map->end()) {
+ // Since we don't currently support multiple profiles, this class
+ // has not been tested against this case.
+ if (map->size() != 0) {
+ return nil;
+ }
+
+ ClearBrowsingDataController* controller =
+ [[self alloc] initWithProfile:profile];
+ it = map->insert(std::make_pair(profile, controller)).first;
+ }
+ return it->second;
+}
+
+- (id)initWithProfile:(Profile*)profile {
+ DCHECK(profile);
+ // Use initWithWindowNibPath:: instead of initWithWindowNibName: so we
+ // can override it in a unit test.
+ NSString *nibpath = [mac_util::MainAppBundle()
+ pathForResource:@"ClearBrowsingData"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ profile_ = profile;
+ observer_.reset(new ClearBrowsingObserver(self));
+ [self initFromPrefs];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ if (remover_) {
+ // We were destroyed while clearing history was in progress. This can only
+ // occur during automated tests (normally the user can't close the dialog
+ // while clearing is in progress as the dialog is modal and not closeable).
+ remover_->RemoveObserver(observer_.get());
+ }
+
+ [super dealloc];
+}
+
+// Run application modal.
+- (void)runModalDialog {
+ // Check again to make sure there is only one window. Since we use
+ // |performSelector:afterDelay:| it is possible for this to somehow be
+ // triggered twice.
+ DCHECK([NSThread isMainThread]);
+ if (![self isWindowLoaded]) {
+ // The Window size in the nib is a min size, loop over the views collecting
+ // the max they grew by, that is how much the window needs to be widened by.
+ CGFloat maxWidthGrowth = 0.0;
+ NSWindow* window = [self window];
+ NSView* contentView = [window contentView];
+ Class widthBasedTweakerClass = [GTMWidthBasedTweaker class];
+ for (id subView in [contentView subviews]) {
+ if ([subView isKindOfClass:widthBasedTweakerClass]) {
+ GTMWidthBasedTweaker* tweaker = subView;
+ CGFloat delta = [tweaker changedWidth];
+ maxWidthGrowth = std::max(maxWidthGrowth, delta);
+ }
+ }
+ if (maxWidthGrowth > 0.0) {
+ NSRect rect = [contentView convertRect:[window frame] fromView:nil];
+ rect.size.width += maxWidthGrowth;
+ rect = [contentView convertRect:rect toView:nil];
+ [window setFrame:rect display:NO];
+ // For some reason the content view is resizing, but some times not
+ // adjusting its origin, so correct it manually.
+ [contentView setFrameOrigin:NSZeroPoint];
+ }
+ // Now start the modal loop.
+ [NSApp runModalForWindow:window];
+ }
+}
+
+- (int)removeMask {
+ int removeMask = 0L;
+ if (clearBrowsingHistory_)
+ removeMask |= BrowsingDataRemover::REMOVE_HISTORY;
+ if (clearDownloadHistory_)
+ removeMask |= BrowsingDataRemover::REMOVE_DOWNLOADS;
+ if (emptyCache_)
+ removeMask |= BrowsingDataRemover::REMOVE_CACHE;
+ if (deleteCookies_)
+ removeMask |= BrowsingDataRemover::REMOVE_COOKIES;
+ if (clearSavedPasswords_)
+ removeMask |= BrowsingDataRemover::REMOVE_PASSWORDS;
+ if (clearFormData_)
+ removeMask |= BrowsingDataRemover::REMOVE_FORM_DATA;
+ return removeMask;
+}
+
+// Called when the user clicks the "clear" button. Do the work and persist
+// the prefs for next time. We don't stop the modal session until we get
+// the callback from the BrowsingDataRemover so the window stays on the screen.
+// While we're working, dim the buttons so the user can't click them.
+- (IBAction)clearData:(id)sender {
+ // Set that we're working so that the buttons disable.
+ [self setIsClearing:YES];
+
+ [self persistToPrefs];
+
+ // BrowsingDataRemover deletes itself when done.
+ remover_ = new BrowsingDataRemover(profile_,
+ static_cast<BrowsingDataRemover::TimePeriod>(timePeriod_),
+ base::Time());
+ remover_->AddObserver(observer_.get());
+ remover_->Remove([self removeMask]);
+}
+
+// Called when the user clicks the cancel button. All we need to do is stop
+// the modal session.
+- (IBAction)cancel:(id)sender {
+ [self closeDialog];
+}
+
+// Called when the user clicks the "Flash Player storage settings" button.
+- (IBAction)openFlashPlayerSettings:(id)sender {
+ // The "Clear Data" dialog is app-modal on OS X. Hence, close it before
+ // opening a tab with flash settings.
+ [self closeDialog];
+
+ Browser* browser = Browser::Create(profile_);
+ browser->OpenURL(GURL(l10n_util::GetStringUTF8(IDS_FLASH_STORAGE_URL)),
+ GURL(), NEW_FOREGROUND_TAB, PageTransition::LINK);
+ browser->window()->Show();
+}
+
+- (void)closeDialog {
+ ProfileControllerMap* map = Singleton<ProfileControllerMap>::get();
+ ProfileControllerMap::iterator it = map->find(profile_);
+ if (it != map->end()) {
+ map->erase(it);
+ }
+ [self autorelease];
+ [[self window] orderOut:self];
+ [NSApp stopModal];
+}
+
+// Initialize the bools from prefs using the setters to be KVO-compliant.
+- (void)initFromPrefs {
+ PrefService* prefs = profile_->GetPrefs();
+ [self setClearBrowsingHistory:
+ prefs->GetBoolean(prefs::kDeleteBrowsingHistory)];
+ [self setClearDownloadHistory:
+ prefs->GetBoolean(prefs::kDeleteDownloadHistory)];
+ [self setEmptyCache:prefs->GetBoolean(prefs::kDeleteCache)];
+ [self setDeleteCookies:prefs->GetBoolean(prefs::kDeleteCookies)];
+ [self setClearSavedPasswords:prefs->GetBoolean(prefs::kDeletePasswords)];
+ [self setClearFormData:prefs->GetBoolean(prefs::kDeleteFormData)];
+ [self setTimePeriod:prefs->GetInteger(prefs::kDeleteTimePeriod)];
+}
+
+// Save the checkbox values to the preferences.
+- (void)persistToPrefs {
+ PrefService* prefs = profile_->GetPrefs();
+ prefs->SetBoolean(prefs::kDeleteBrowsingHistory,
+ [self clearBrowsingHistory]);
+ prefs->SetBoolean(prefs::kDeleteDownloadHistory,
+ [self clearDownloadHistory]);
+ prefs->SetBoolean(prefs::kDeleteCache, [self emptyCache]);
+ prefs->SetBoolean(prefs::kDeleteCookies, [self deleteCookies]);
+ prefs->SetBoolean(prefs::kDeletePasswords, [self clearSavedPasswords]);
+ prefs->SetBoolean(prefs::kDeleteFormData, [self clearFormData]);
+ prefs->SetInteger(prefs::kDeleteTimePeriod, [self timePeriod]);
+}
+
+// Called when the data remover object is done with its work. Close the window.
+// The remover will delete itself. End the modal session at this point.
+- (void)dataRemoverDidFinish {
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ int removeMask = [self removeMask];
+ NSDictionary* userInfo =
+ [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:removeMask]
+ forKey:kClearBrowsingDataControllerRemoveMask];
+ [center postNotificationName:kClearBrowsingDataControllerDidDelete
+ object:self
+ userInfo:userInfo];
+
+ [self closeDialog];
+ [[self window] orderOut:self];
+ [self setIsClearing:NO];
+ remover_ = NULL;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/clear_browsing_data_controller_unittest.mm b/chrome/browser/ui/cocoa/clear_browsing_data_controller_unittest.mm
new file mode 100644
index 0000000..208cfa4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/clear_browsing_data_controller_unittest.mm
@@ -0,0 +1,149 @@
+// 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"
+#include "chrome/browser/browsing_data_remover.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/clear_browsing_data_controller.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/common/pref_names.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+
+namespace {
+
+class ClearBrowsingDataControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ // Set up some interesting prefs:
+ PrefService* prefs = helper_.profile()->GetPrefs();
+ prefs->SetBoolean(prefs::kDeleteBrowsingHistory, true);
+ prefs->SetBoolean(prefs::kDeleteDownloadHistory, false);
+ prefs->SetBoolean(prefs::kDeleteCache, true);
+ prefs->SetBoolean(prefs::kDeleteCookies, false);
+ prefs->SetBoolean(prefs::kDeletePasswords, true);
+ prefs->SetBoolean(prefs::kDeleteFormData, false);
+ prefs->SetInteger(prefs::kDeleteTimePeriod,
+ BrowsingDataRemover::FOUR_WEEKS);
+ controller_ =
+ [ClearBrowsingDataController controllerForProfile:helper_.profile()];
+ }
+
+ virtual void TearDown() {
+ [controller_ closeDialog];
+ CocoaTest::TearDown();
+ }
+
+ BrowserTestHelper helper_;
+ ClearBrowsingDataController* controller_;
+};
+
+TEST_F(ClearBrowsingDataControllerTest, InitialState) {
+ // Check properties match the prefs set above:
+ EXPECT_TRUE([controller_ clearBrowsingHistory]);
+ EXPECT_FALSE([controller_ clearDownloadHistory]);
+ EXPECT_TRUE([controller_ emptyCache]);
+ EXPECT_FALSE([controller_ deleteCookies]);
+ EXPECT_TRUE([controller_ clearSavedPasswords]);
+ EXPECT_FALSE([controller_ clearFormData]);
+ EXPECT_EQ(BrowsingDataRemover::FOUR_WEEKS,
+ [controller_ timePeriod]);
+}
+
+TEST_F(ClearBrowsingDataControllerTest, InitialRemoveMask) {
+ // Check that the remove-mask matches the initial properties:
+ EXPECT_EQ(BrowsingDataRemover::REMOVE_HISTORY |
+ BrowsingDataRemover::REMOVE_CACHE |
+ BrowsingDataRemover::REMOVE_PASSWORDS,
+ [controller_ removeMask]);
+}
+
+TEST_F(ClearBrowsingDataControllerTest, ModifiedRemoveMask) {
+ // Invert all properties and check that the remove-mask is still correct:
+ [controller_ setClearBrowsingHistory:false];
+ [controller_ setClearDownloadHistory:true];
+ [controller_ setEmptyCache:false];
+ [controller_ setDeleteCookies:true];
+ [controller_ setClearSavedPasswords:false];
+ [controller_ setClearFormData:true];
+
+ EXPECT_EQ(BrowsingDataRemover::REMOVE_DOWNLOADS |
+ BrowsingDataRemover::REMOVE_COOKIES |
+ BrowsingDataRemover::REMOVE_FORM_DATA,
+ [controller_ removeMask]);
+}
+
+TEST_F(ClearBrowsingDataControllerTest, EmptyRemoveMask) {
+ // Clear all properties and check that the remove-mask is zero:
+ [controller_ setClearBrowsingHistory:false];
+ [controller_ setClearDownloadHistory:false];
+ [controller_ setEmptyCache:false];
+ [controller_ setDeleteCookies:false];
+ [controller_ setClearSavedPasswords:false];
+ [controller_ setClearFormData:false];
+
+ EXPECT_EQ(0,
+ [controller_ removeMask]);
+}
+
+TEST_F(ClearBrowsingDataControllerTest, PersistToPrefs) {
+ // Change some settings and store to prefs:
+ [controller_ setClearBrowsingHistory:false];
+ [controller_ setClearDownloadHistory:true];
+ [controller_ persistToPrefs];
+
+ // Test that the modified settings were stored to prefs:
+ PrefService* prefs = helper_.profile()->GetPrefs();
+ EXPECT_FALSE(prefs->GetBoolean(prefs::kDeleteBrowsingHistory));
+ EXPECT_TRUE(prefs->GetBoolean(prefs::kDeleteDownloadHistory));
+
+ // Make sure the rest of the prefs didn't change:
+ EXPECT_TRUE(prefs->GetBoolean(prefs::kDeleteCache));
+ EXPECT_FALSE(prefs->GetBoolean(prefs::kDeleteCookies));
+ EXPECT_TRUE(prefs->GetBoolean(prefs::kDeletePasswords));
+ EXPECT_FALSE(prefs->GetBoolean(prefs::kDeleteFormData));
+ EXPECT_EQ(BrowsingDataRemover::FOUR_WEEKS,
+ prefs->GetInteger(prefs::kDeleteTimePeriod));
+}
+
+TEST_F(ClearBrowsingDataControllerTest, SameControllerForProfile) {
+ ClearBrowsingDataController* controller =
+ [ClearBrowsingDataController controllerForProfile:helper_.profile()];
+ EXPECT_EQ(controller_, controller);
+}
+
+TEST_F(ClearBrowsingDataControllerTest, DataRemoverDidFinish) {
+ id observer = [OCMockObject observerMock];
+ // Don't use |controller_| as the object because it will free itself twice
+ // because both |-dataRemoverDidFinish| and TearDown() call |-closeDialog|.
+ ClearBrowsingDataController* controller =
+ [[ClearBrowsingDataController alloc] initWithProfile:helper_.profile()];
+
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center addMockObserver:observer
+ name:kClearBrowsingDataControllerDidDelete
+ object:controller];
+
+ int mask = [controller removeMask];
+ NSDictionary* expectedInfo =
+ [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:mask]
+ forKey:kClearBrowsingDataControllerRemoveMask];
+ [[observer expect]
+ notificationWithName:kClearBrowsingDataControllerDidDelete
+ object:controller
+ userInfo:expectedInfo];
+
+ // This calls |-closeDialog| and cleans the controller up.
+ [controller dataRemoverDidFinish];
+
+ [observer verify];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/clickhold_button_cell.h b/chrome/browser/ui/cocoa/clickhold_button_cell.h
new file mode 100644
index 0000000..89218cf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/clickhold_button_cell.h
@@ -0,0 +1,48 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_CLICKHOLD_BUTTON_CELL_H_
+#define CHROME_BROWSER_UI_COCOA_CLICKHOLD_BUTTON_CELL_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/gradient_button_cell.h"
+
+// A button cell that implements "click hold" behavior after a specified delay
+// or after dragging. If click-hold is never enabled (e.g., if
+// |-setEnableClickHold:| is never called), this behaves like a normal button.
+@interface ClickHoldButtonCell : GradientButtonCell {
+ @private
+ BOOL enableClickHold_;
+ NSTimeInterval clickHoldTimeout_;
+ id clickHoldTarget_; // Weak.
+ SEL clickHoldAction_;
+ BOOL trackOnlyInRect_;
+ BOOL activateOnDrag_;
+}
+
+// Enable click-hold? Default: NO.
+@property(assign, nonatomic) BOOL enableClickHold;
+
+// Timeout is in seconds (at least 0.0, at most 5; 0.0 means that the button
+// will always have its click-hold action activated immediately on press).
+// Default: 0.25 (a guess at a Cocoa-ish value).
+@property(assign, nonatomic) NSTimeInterval clickHoldTimeout;
+
+// Track only in the frame rectangle? Default: NO.
+@property(assign, nonatomic) BOOL trackOnlyInRect;
+
+// Activate (click-hold) immediately on a sufficiently-large drag (if not,
+// always wait for timeout)? Default: YES.
+@property(assign, nonatomic) BOOL activateOnDrag;
+
+// Defines what to do when click-held (as per usual action/target).
+@property(assign, nonatomic) id clickHoldTarget;
+@property(assign, nonatomic) SEL clickHoldAction;
+
+@end // @interface ClickHoldButtonCell
+
+#endif // CHROME_BROWSER_UI_COCOA_CLICKHOLD_BUTTON_CELL_H_
diff --git a/chrome/browser/ui/cocoa/clickhold_button_cell.mm b/chrome/browser/ui/cocoa/clickhold_button_cell.mm
new file mode 100644
index 0000000..9b4424d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/clickhold_button_cell.mm
@@ -0,0 +1,190 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/clickhold_button_cell.h"
+
+#include "base/logging.h"
+
+// Minimum and maximum click-hold timeout.
+static const NSTimeInterval kMinTimeout = 0.0;
+static const NSTimeInterval kMaxTimeout = 5.0;
+
+// Drag distance threshold to activate click-hold; should be >= 0.
+static const CGFloat kDragDistThreshold = 2.5;
+
+// See |-resetToDefaults| (and header file) for other default values.
+
+@interface ClickHoldButtonCell (Private)
+- (void)resetToDefaults;
+@end // @interface ClickHoldButtonCell (Private)
+
+@implementation ClickHoldButtonCell
+
+// Overrides:
+
++ (BOOL)prefersTrackingUntilMouseUp {
+ return NO;
+}
+
+- (id)init {
+ if ((self = [super init]))
+ [self resetToDefaults];
+ return self;
+}
+
+- (id)initWithCoder:(NSCoder*)decoder {
+ if ((self = [super initWithCoder:decoder]))
+ [self resetToDefaults];
+ return self;
+}
+
+- (id)initImageCell:(NSImage*)image {
+ if ((self = [super initImageCell:image]))
+ [self resetToDefaults];
+ return self;
+}
+
+- (id)initTextCell:(NSString*)string {
+ if ((self = [super initTextCell:string]))
+ [self resetToDefaults];
+ return self;
+}
+
+- (BOOL)startTrackingAt:(NSPoint)startPoint
+ inView:(NSView*)controlView {
+ return enableClickHold_ ? YES :
+ [super startTrackingAt:startPoint
+ inView:controlView];
+}
+
+- (BOOL)continueTracking:(NSPoint)lastPoint
+ at:(NSPoint)currentPoint
+ inView:(NSView*)controlView {
+ return enableClickHold_ ? YES :
+ [super continueTracking:lastPoint
+ at:currentPoint
+ inView:controlView];
+}
+
+- (BOOL)trackMouse:(NSEvent*)originalEvent
+ inRect:(NSRect)cellFrame
+ ofView:(NSView*)controlView
+ untilMouseUp:(BOOL)untilMouseUp {
+ if (!enableClickHold_) {
+ return [super trackMouse:originalEvent
+ inRect:cellFrame
+ ofView:controlView
+ untilMouseUp:untilMouseUp];
+ }
+
+ // If doing click-hold, track the mouse ourselves.
+ NSPoint currPoint = [controlView convertPoint:[originalEvent locationInWindow]
+ fromView:nil];
+ NSPoint lastPoint = currPoint;
+ NSPoint firstPoint = currPoint;
+ NSTimeInterval timeout =
+ MAX(MIN(clickHoldTimeout_, kMaxTimeout), kMinTimeout);
+ NSDate* clickHoldBailTime = [NSDate dateWithTimeIntervalSinceNow:timeout];
+
+ if (![self startTrackingAt:currPoint inView:controlView])
+ return NO;
+
+ enum {
+ kContinueTrack, kStopClickHold, kStopMouseUp, kStopLeftRect, kStopNoContinue
+ } state = kContinueTrack;
+ do {
+ NSEvent* event = [NSApp nextEventMatchingMask:(NSLeftMouseDraggedMask |
+ NSLeftMouseUpMask)
+ untilDate:clickHoldBailTime
+ inMode:NSEventTrackingRunLoopMode
+ dequeue:YES];
+ currPoint = [controlView convertPoint:[event locationInWindow]
+ fromView:nil];
+
+ // Time-out.
+ if (!event) {
+ state = kStopClickHold;
+
+ // Drag? (If distance meets threshold.)
+ } else if (activateOnDrag_ && ([event type] == NSLeftMouseDragged)) {
+ CGFloat dx = currPoint.x - firstPoint.x;
+ CGFloat dy = currPoint.y - firstPoint.y;
+ if ((dx*dx + dy*dy) >= (kDragDistThreshold*kDragDistThreshold))
+ state = kStopClickHold;
+
+ // Mouse up.
+ } else if ([event type] == NSLeftMouseUp) {
+ state = kStopMouseUp;
+
+ // Stop tracking if mouse left frame rectangle (if requested to do so).
+ } else if (trackOnlyInRect_ && ![controlView mouse:currPoint
+ inRect:cellFrame]) {
+ state = kStopLeftRect;
+
+ // Stop tracking if instructed to.
+ } else if (![self continueTracking:lastPoint
+ at:currPoint
+ inView:controlView]) {
+ state = kStopNoContinue;
+ }
+
+ lastPoint = currPoint;
+ } while (state == kContinueTrack);
+
+ [self stopTracking:lastPoint
+ at:lastPoint
+ inView:controlView
+ mouseIsUp:NO];
+
+ switch (state) {
+ case kStopClickHold:
+ if (clickHoldAction_) {
+ [static_cast<NSControl*>(controlView) sendAction:clickHoldAction_
+ to:clickHoldTarget_];
+ }
+ return YES;
+
+ case kStopMouseUp:
+ if ([self action]) {
+ [static_cast<NSControl*>(controlView) sendAction:[self action]
+ to:[self target]];
+ }
+ return YES;
+
+ case kStopLeftRect:
+ case kStopNoContinue:
+ return NO;
+
+ default:
+ NOTREACHED() << "Unknown terminating state!";
+ }
+
+ return NO;
+}
+
+// Accessors and mutators:
+
+@synthesize enableClickHold = enableClickHold_;
+@synthesize clickHoldTimeout = clickHoldTimeout_;
+@synthesize trackOnlyInRect = trackOnlyInRect_;
+@synthesize activateOnDrag = activateOnDrag_;
+@synthesize clickHoldTarget = clickHoldTarget_;
+@synthesize clickHoldAction = clickHoldAction_;
+
+@end // @implementation ClickHoldButtonCell
+
+@implementation ClickHoldButtonCell (Private)
+
+// Resets various members to defaults indicated in the header file. (Those
+// without indicated defaults are *not* touched.) Please keep the values below
+// in sync with the header file, and please be aware of side-effects on code
+// which relies on the "published" defaults.
+- (void)resetToDefaults {
+ [self setEnableClickHold:NO];
+ [self setClickHoldTimeout:0.25];
+ [self setTrackOnlyInRect:NO];
+ [self setActivateOnDrag:YES];
+}
+
+@end // @implementation ClickHoldButtonCell (Private)
diff --git a/chrome/browser/ui/cocoa/clickhold_button_cell_unittest.mm b/chrome/browser/ui/cocoa/clickhold_button_cell_unittest.mm
new file mode 100644
index 0000000..7ccc773
--- /dev/null
+++ b/chrome/browser/ui/cocoa/clickhold_button_cell_unittest.mm
@@ -0,0 +1,51 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/clickhold_button_cell.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class ClickHoldButtonCellTest : public CocoaTest {
+ public:
+ ClickHoldButtonCellTest() {
+ NSRect frame = NSMakeRect(0, 0, 50, 30);
+ scoped_nsobject<NSButton> view([[NSButton alloc] initWithFrame:frame]);
+ view_ = view.get();
+ scoped_nsobject<ClickHoldButtonCell> cell(
+ [[ClickHoldButtonCell alloc] initTextCell:@"Testing"]);
+ [view_ setCell:cell.get()];
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ NSButton* view_;
+};
+
+TEST_VIEW(ClickHoldButtonCellTest, view_)
+
+// Test default values; make sure they are what they should be.
+TEST_F(ClickHoldButtonCellTest, Defaults) {
+ ClickHoldButtonCell* cell = static_cast<ClickHoldButtonCell*>([view_ cell]);
+ ASSERT_TRUE([cell isKindOfClass:[ClickHoldButtonCell class]]);
+
+ EXPECT_FALSE([cell enableClickHold]);
+
+ NSTimeInterval clickHoldTimeout = [cell clickHoldTimeout];
+ EXPECT_GE(clickHoldTimeout, 0.15); // Check for a "Cocoa-ish" value.
+ EXPECT_LE(clickHoldTimeout, 0.35);
+
+ EXPECT_FALSE([cell trackOnlyInRect]);
+ EXPECT_TRUE([cell activateOnDrag]);
+}
+
+// TODO(viettrungluu): (1) Enable click-hold and figure out how to test the
+// tracking loop (i.e., |-trackMouse:...|), which is the nontrivial part.
+// (2) Test various initialization code paths (in particular, loading from nib).
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/cocoa_test_helper.h b/chrome/browser/ui/cocoa/cocoa_test_helper.h
new file mode 100644
index 0000000..3431925
--- /dev/null
+++ b/chrome/browser/ui/cocoa/cocoa_test_helper.h
@@ -0,0 +1,153 @@
+// 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_UI_COCOA_COCOA_TEST_HELPER_H_
+#define CHROME_BROWSER_UI_COCOA_COCOA_TEST_HELPER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/chrome_application_mac.h"
+#include "base/debug_util.h"
+#include "base/mac_util.h"
+#include "base/path_service.h"
+#import "base/mac/scoped_nsautorelease_pool.h"
+#import "base/scoped_nsobject.h"
+#include "chrome/common/chrome_constants.h"
+#include "testing/platform_test.h"
+
+// Background windows normally will not display things such as focus
+// rings. This class allows -isKeyWindow to be manipulated to test
+// such things.
+@interface CocoaTestHelperWindow : NSWindow {
+ @private
+ BOOL pretendIsKeyWindow_;
+}
+
+// Init a borderless non-deferred window with a backing store.
+- (id)initWithContentRect:(NSRect)contentRect;
+
+// Init with a default frame.
+- (id)init;
+
+// Sets the responder passed in as first responder, and sets the window
+// so that it will return "YES" if asked if it key window. It does not actually
+// make the window key.
+- (void)makePretendKeyWindowAndSetFirstResponder:(NSResponder*)responder;
+
+// Clears the first responder duty for the window and returns the window
+// to being non-key.
+- (void)clearPretendKeyWindowAndFirstResponder;
+
+// Set value to return for -isKeyWindow.
+- (void)setPretendIsKeyWindow:(BOOL)isKeyWindow;
+
+- (BOOL)isKeyWindow;
+
+@end
+
+// A test class that all tests that depend on AppKit should inherit from.
+// Sets up NSApplication and paths correctly, and makes sure that any windows
+// created in the test are closed down properly by the test. If you need to
+// inherit from a different test class, but need to set up the AppKit runtime
+// environment, you can call BootstrapCocoa directly from your test class. You
+// will have to deal with windows on your own though.
+class CocoaTest : public PlatformTest {
+ public:
+ // Sets up AppKit and paths correctly for unit tests. If you can't inherit
+ // from CocoaTest but are going to be using any AppKit features directly,
+ // or indirectly, you should be calling this from the c'tor or SetUp methods
+ // of your test class.
+ static void BootstrapCocoa();
+
+ CocoaTest();
+ virtual ~CocoaTest();
+
+ // Must be called by subclasses that override TearDown. We verify that it
+ // is called in our destructor. Takes care of making sure that all windows
+ // are closed off correctly. If your tests open windows, they must be sure
+ // to close them before CocoaTest::TearDown is called. A standard way of doing
+ // this would be to create them in SetUp (after calling CocoaTest::Setup) and
+ // then close them in TearDown before calling CocoaTest::TearDown.
+ virtual void TearDown();
+
+ // Retuns a test window that can be used by views and other UI objects
+ // as part of their tests. Is created lazily, and will be closed correctly
+ // in CocoaTest::TearDown. Note that it is a CocoaTestHelperWindow which
+ // has special handling for being Key.
+ CocoaTestHelperWindow* test_window();
+
+ private:
+ // Return a set of currently open windows. Avoiding NSArray so
+ // contents aren't retained, the pointer values can only be used for
+ // comparison purposes. Using std::set to make progress-checking
+ // convenient.
+ static std::set<NSWindow*> ApplicationWindows();
+
+ // Return a set of windows which are in |ApplicationWindows()| but
+ // not |initial_windows_|.
+ std::set<NSWindow*> WindowsLeft();
+
+ bool called_tear_down_;
+ base::mac::ScopedNSAutoreleasePool pool_;
+
+ // Windows which existed at the beginning of the test.
+ std::set<NSWindow*> initial_windows_;
+
+ // Strong. Lazily created. This isn't wrapped in a scoped_nsobject because
+ // we want to call [close] to destroy it rather than calling [release]. We
+ // want to verify that [close] is actually removing our window and that it's
+ // not hanging around because releaseWhenClosed was set to "no" on the window.
+ // It isn't wrapped in a different wrapper class to close it because we
+ // need to close it at a very specific time; just before we enter our clean
+ // up loop in TearDown.
+ CocoaTestHelperWindow* test_window_;
+};
+
+// A macro defining a standard set of tests to run on a view. Since we can't
+// inherit tests, this macro saves us a lot of duplicate code. Handles simply
+// displaying the view to make sure it won't crash, as well as removing it
+// from a window. All tests that work with NSView subclasses and/or
+// NSViewController subclasses should use it.
+#define TEST_VIEW(test_fixture, test_view) \
+ TEST_F(test_fixture, AddRemove##test_fixture) { \
+ scoped_nsobject<NSView> view([test_view retain]); \
+ EXPECT_EQ([test_window() contentView], [view superview]); \
+ [view removeFromSuperview]; \
+ EXPECT_FALSE([view superview]); \
+ } \
+ TEST_F(test_fixture, Display##test_fixture) { \
+ [test_view display]; \
+ }
+
+// A macro which determines the proper float epsilon for a CGFloat.
+#if CGFLOAT_IS_DOUBLE
+#define CGFLOAT_EPSILON DBL_EPSILON
+#else
+#define CGFLOAT_EPSILON FLT_EPSILON
+#endif
+
+// A macro which which determines if two CGFloats are equal taking a
+// proper epsilon into consideration.
+#define CGFLOAT_EQ(expected, actual) \
+ (actual >= (expected - CGFLOAT_EPSILON) && \
+ actual <= (expected + CGFLOAT_EPSILON))
+
+// A test support macro which ascertains if two CGFloats are equal.
+#define EXPECT_CGFLOAT_EQ(expected, actual) \
+ EXPECT_TRUE(CGFLOAT_EQ(expected, actual)) << \
+ expected << " != " << actual
+
+// A test support macro which compares two NSRects for equality taking
+// the float epsilon into consideration.
+#define EXPECT_NSRECT_EQ(expected, actual) \
+ EXPECT_TRUE(CGFLOAT_EQ(expected.origin.x, actual.origin.x) && \
+ CGFLOAT_EQ(expected.origin.y, actual.origin.y) && \
+ CGFLOAT_EQ(expected.size.width, actual.size.width) && \
+ CGFLOAT_EQ(expected.size.height, actual.size.height)) << \
+ "Rects do not match: " << \
+ [NSStringFromRect(expected) UTF8String] << \
+ " != " << [NSStringFromRect(actual) UTF8String]
+
+#endif // CHROME_BROWSER_UI_COCOA_COCOA_TEST_HELPER_H_
diff --git a/chrome/browser/ui/cocoa/cocoa_test_helper.mm b/chrome/browser/ui/cocoa/cocoa_test_helper.mm
new file mode 100644
index 0000000..2cd2cc0
--- /dev/null
+++ b/chrome/browser/ui/cocoa/cocoa_test_helper.mm
@@ -0,0 +1,205 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+
+#include "base/debug/debugger.h"
+#include "base/logging.h"
+#include "base/test/test_timeouts.h"
+#import "chrome/browser/chrome_browser_application_mac.h"
+
+@implementation CocoaTestHelperWindow
+
+- (id)initWithContentRect:(NSRect)contentRect {
+ return [self initWithContentRect:contentRect
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:NO];
+}
+
+- (id)init {
+ return [self initWithContentRect:NSMakeRect(0, 0, 800, 600)];
+}
+
+- (void)dealloc {
+ // Just a good place to put breakpoints when having problems with
+ // unittests and CocoaTestHelperWindow.
+ [super dealloc];
+}
+
+- (void)makePretendKeyWindowAndSetFirstResponder:(NSResponder*)responder {
+ EXPECT_TRUE([self makeFirstResponder:responder]);
+ [self setPretendIsKeyWindow:YES];
+}
+
+- (void)clearPretendKeyWindowAndFirstResponder {
+ [self setPretendIsKeyWindow:NO];
+ EXPECT_TRUE([self makeFirstResponder:NSApp]);
+}
+
+- (void)setPretendIsKeyWindow:(BOOL)flag {
+ pretendIsKeyWindow_ = flag;
+}
+
+- (BOOL)isKeyWindow {
+ return pretendIsKeyWindow_;
+}
+
+@end
+
+CocoaTest::CocoaTest() : called_tear_down_(false), test_window_(nil) {
+ BootstrapCocoa();
+
+ // Set the duration of AppKit-evaluated animations (such as frame changes)
+ // to zero for testing purposes. That way they take effect immediately.
+ [[NSAnimationContext currentContext] setDuration:0.0];
+
+ // The above does not affect window-resize time, such as for an
+ // attached sheet dropping in. Set that duration for the current
+ // process (this is not persisted). Empirically, the value of 0.0
+ // is ignored.
+ NSDictionary* dict =
+ [NSDictionary dictionaryWithObject:@"0.01" forKey:@"NSWindowResizeTime"];
+ [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
+
+ // Collect the list of windows that were open when the test started so
+ // that we don't wait for them to close in TearDown. Has to be done
+ // after BootstrapCocoa is called.
+ initial_windows_ = ApplicationWindows();
+}
+
+CocoaTest::~CocoaTest() {
+ // Must call CocoaTest's teardown from your overrides.
+ DCHECK(called_tear_down_);
+}
+
+void CocoaTest::BootstrapCocoa() {
+ // Look in the framework bundle for resources.
+ FilePath path;
+ PathService::Get(base::DIR_EXE, &path);
+ path = path.Append(chrome::kFrameworkName);
+ mac_util::SetOverrideAppBundlePath(path);
+
+ // Bootstrap Cocoa. It's very unhappy without this.
+ [CrApplication sharedApplication];
+}
+
+void CocoaTest::TearDown() {
+ called_tear_down_ = true;
+ // Call close on our test_window to clean it up if one was opened.
+ [test_window_ close];
+ test_window_ = nil;
+
+ // Recycle the pool to clean up any stuff that was put on the
+ // autorelease pool due to window or windowcontroller closures.
+ pool_.Recycle();
+
+ // Some controls (NSTextFields, NSComboboxes etc) use
+ // performSelector:withDelay: to clean up drag handlers and other
+ // things (Radar 5851458 "Closing a window with a NSTextView in it
+ // should get rid of it immediately"). The event loop must be spun
+ // to get everything cleaned up correctly. It normally only takes
+ // one to two spins through the event loop to see a change.
+
+ // NOTE(shess): Under valgrind, -nextEventMatchingMask:* in one test
+ // needed to run twice, once taking .2 seconds, the next time .6
+ // seconds. The loop exit condition attempts to be scalable.
+
+ // Get the set of windows which weren't present when the test
+ // started.
+ std::set<NSWindow*> windows_left(WindowsLeft());
+
+ while (windows_left.size() > 0) {
+ // Cover delayed actions by spinning the loop at least once after
+ // this timeout.
+ const NSTimeInterval kCloseTimeoutSeconds =
+ TestTimeouts::action_timeout_ms() / 1000.0;
+
+ // Cover chains of delayed actions by spinning the loop at least
+ // this many times.
+ const int kCloseSpins = 3;
+
+ // Track the set of remaining windows so that everything can be
+ // reset if progress is made.
+ std::set<NSWindow*> still_left = windows_left;
+
+ NSDate* start_date = [NSDate date];
+ bool one_more_time = true;
+ int spins = 0;
+ while (still_left.size() == windows_left.size() &&
+ (spins < kCloseSpins || one_more_time)) {
+ // Check the timeout before pumping events, so that we'll spin
+ // the loop once after the timeout.
+ one_more_time = ([start_date timeIntervalSinceNow] > -kCloseTimeoutSeconds);
+
+ // Autorelease anything thrown up by the event loop.
+ {
+ base::mac::ScopedNSAutoreleasePool pool;
+ ++spins;
+ NSEvent *next_event = [NSApp nextEventMatchingMask:NSAnyEventMask
+ untilDate:nil
+ inMode:NSDefaultRunLoopMode
+ dequeue:YES];
+ [NSApp sendEvent:next_event];
+ [NSApp updateWindows];
+ }
+
+ // Refresh the outstanding windows.
+ still_left = WindowsLeft();
+ }
+
+ // If no progress is being made, log a failure and continue.
+ if (still_left.size() == windows_left.size()) {
+ // NOTE(shess): Failing this expectation means that the test
+ // opened windows which have not been fully released. Either
+ // there is a leak, or perhaps one of |kCloseTimeoutSeconds| or
+ // |kCloseSpins| needs adjustment.
+ EXPECT_EQ(0U, windows_left.size());
+ for (std::set<NSWindow*>::iterator iter = windows_left.begin();
+ iter != windows_left.end(); ++iter) {
+ const char* desc = [[*iter description] UTF8String];
+ LOG(WARNING) << "Didn't close window " << desc;
+ }
+ break;
+ }
+
+ windows_left = still_left;
+ }
+ PlatformTest::TearDown();
+}
+
+std::set<NSWindow*> CocoaTest::ApplicationWindows() {
+ // This must NOT retain the windows it is returning.
+ std::set<NSWindow*> windows;
+
+ // Must create a pool here because [NSApp windows] has created an array
+ // with retains on all the windows in it.
+ base::mac::ScopedNSAutoreleasePool pool;
+ NSArray *appWindows = [NSApp windows];
+ for (NSWindow *window in appWindows) {
+ windows.insert(window);
+ }
+ return windows;
+}
+
+std::set<NSWindow*> CocoaTest::WindowsLeft() {
+ const std::set<NSWindow*> windows(ApplicationWindows());
+ std::set<NSWindow*> windows_left;
+ std::set_difference(windows.begin(), windows.end(),
+ initial_windows_.begin(), initial_windows_.end(),
+ std::inserter(windows_left, windows_left.begin()));
+ return windows_left;
+}
+
+CocoaTestHelperWindow* CocoaTest::test_window() {
+ if (!test_window_) {
+ test_window_ = [[CocoaTestHelperWindow alloc] init];
+ if (base::debug::BeingDebugged()) {
+ [test_window_ orderFront:nil];
+ } else {
+ [test_window_ orderBack:nil];
+ }
+ }
+ return test_window_;
+}
diff --git a/chrome/browser/ui/cocoa/collected_cookies_mac.h b/chrome/browser/ui/cocoa/collected_cookies_mac.h
new file mode 100644
index 0000000..23647ee
--- /dev/null
+++ b/chrome/browser/ui/cocoa/collected_cookies_mac.h
@@ -0,0 +1,123 @@
+// 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/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/cookies_tree_model.h"
+#include "chrome/browser/ui/cocoa/constrained_window_mac.h"
+#import "chrome/browser/ui/cocoa/cookie_tree_node.h"
+#include "chrome/common/notification_registrar.h"
+
+@class CollectedCookiesWindowController;
+@class VerticalGradientView;
+class TabContents;
+
+// The constrained window delegate reponsible for managing the collected
+// cookies dialog.
+class CollectedCookiesMac : public ConstrainedWindowMacDelegateCustomSheet,
+ public NotificationObserver {
+ public:
+ CollectedCookiesMac(NSWindow* parent, TabContents* tab_contents);
+
+ void OnSheetDidEnd(NSWindow* sheet);
+
+ // ConstrainedWindowMacDelegateCustomSheet implementation.
+ virtual void DeleteDelegate();
+
+ private:
+ virtual ~CollectedCookiesMac();
+
+ // NotificationObserver implementation.
+ void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+
+ NotificationRegistrar registrar_;
+
+ ConstrainedWindow* window_;
+
+ TabContents* tab_contents_;
+
+ CollectedCookiesWindowController* sheet_controller_;
+
+ DISALLOW_COPY_AND_ASSIGN(CollectedCookiesMac);
+};
+
+// Controller for the collected cookies dialog. This class stores an internal
+// copy of the CookiesTreeModel but with Cocoa-converted values (NSStrings and
+// NSImages instead of std::strings and SkBitmaps). Doing this allows us to use
+// bindings for the interface. Changes are pushed to this internal model via a
+// very thin bridge (see cookies_window_controller.h).
+@interface CollectedCookiesWindowController : NSWindowController
+ <NSOutlineViewDelegate,
+ NSWindowDelegate> {
+ @private
+ // Platform-independent model.
+ scoped_ptr<CookiesTreeModel> allowedTreeModel_;
+ scoped_ptr<CookiesTreeModel> blockedTreeModel_;
+
+ // Cached array of icons.
+ scoped_nsobject<NSMutableArray> icons_;
+
+ // Our Cocoa copy of the model.
+ scoped_nsobject<CocoaCookieTreeNode> cocoaAllowedTreeModel_;
+ scoped_nsobject<CocoaCookieTreeNode> cocoaBlockedTreeModel_;
+
+ BOOL allowedCookiesButtonsEnabled_;
+ BOOL blockedCookiesButtonsEnabled_;
+
+ IBOutlet NSTreeController* allowedTreeController_;
+ IBOutlet NSTreeController* blockedTreeController_;
+ IBOutlet NSOutlineView* allowedOutlineView_;
+ IBOutlet NSOutlineView* blockedOutlineView_;
+ IBOutlet VerticalGradientView* infoBar_;
+ IBOutlet NSImageView* infoBarIcon_;
+ IBOutlet NSTextField* infoBarText_;
+ IBOutlet NSSplitView* splitView_;
+ IBOutlet NSScrollView* lowerScrollView_;
+ IBOutlet NSTextField* blockedCookiesText_;
+
+ scoped_nsobject<NSViewAnimation> animation_;
+
+ TabContents* tabContents_; // weak
+
+ BOOL infoBarVisible_;
+}
+@property (readonly, nonatomic) NSTreeController* allowedTreeController;
+@property (readonly, nonatomic) NSTreeController* blockedTreeController;
+
+@property (assign, nonatomic) BOOL allowedCookiesButtonsEnabled;
+@property (assign, nonatomic) BOOL blockedCookiesButtonsEnabled;
+
+// Designated initializer. TabContents cannot be NULL.
+- (id)initWithTabContents:(TabContents*)tabContents;
+
+// Closes the sheet and ends the modal loop. This will also cleanup the memory.
+- (IBAction)closeSheet:(id)sender;
+
+- (IBAction)allowOrigin:(id)sender;
+- (IBAction)allowForSessionFromOrigin:(id)sender;
+- (IBAction)blockOrigin:(id)sender;
+
+// NSSplitView delegate methods:
+- (CGFloat) splitView:(NSSplitView *)sender
+ constrainMinCoordinate:(CGFloat)proposedMin
+ ofSubviewAt:(NSInteger)offset;
+- (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview;
+
+// Returns the cocoaAllowedTreeModel_ and cocoaBlockedTreeModel_.
+- (CocoaCookieTreeNode*)cocoaAllowedTreeModel;
+- (CocoaCookieTreeNode*)cocoaBlockedTreeModel;
+- (void)setCocoaAllowedTreeModel:(CocoaCookieTreeNode*)model;
+- (void)setCocoaBlockedTreeModel:(CocoaCookieTreeNode*)model;
+
+// Returns the allowedTreeModel_ and blockedTreeModel_.
+- (CookiesTreeModel*)allowedTreeModel;
+- (CookiesTreeModel*)blockedTreeModel;
+
+- (void)loadTreeModelFromTabContents;
+@end
diff --git a/chrome/browser/ui/cocoa/collected_cookies_mac.mm b/chrome/browser/ui/cocoa/collected_cookies_mac.mm
new file mode 100644
index 0000000..7a79680
--- /dev/null
+++ b/chrome/browser/ui/cocoa/collected_cookies_mac.mm
@@ -0,0 +1,499 @@
+// 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/ui/cocoa/collected_cookies_mac.h"
+
+#include <vector>
+
+#include "app/l10n_util_mac.h"
+#include "app/resource_bundle.h"
+#import "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#import "chrome/browser/ui/cocoa/vertical_gradient_view.h"
+#include "chrome/common/notification_service.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+#include "third_party/apple/ImageAndTextCell.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+namespace {
+// Colors for the infobar.
+const double kBannerGradientColorTop[3] =
+ {255.0 / 255.0, 242.0 / 255.0, 183.0 / 255.0};
+const double kBannerGradientColorBottom[3] =
+ {250.0 / 255.0, 230.0 / 255.0, 145.0 / 255.0};
+const double kBannerStrokeColor = 135.0 / 255.0;
+
+// Minimal height for the collected cookies dialog.
+const CGFloat kMinCollectedCookiesViewHeight = 116;
+} // namespace
+
+#pragma mark Bridge between the constrained window delegate and the sheet
+
+// The delegate used to forward the events from the sheet to the constrained
+// window delegate.
+@interface CollectedCookiesSheetBridge : NSObject {
+ CollectedCookiesMac* collectedCookies_; // weak
+}
+- (id)initWithCollectedCookiesMac:(CollectedCookiesMac*)collectedCookies;
+- (void)sheetDidEnd:(NSWindow*)sheet
+ returnCode:(int)returnCode
+ contextInfo:(void*)contextInfo;
+@end
+
+@implementation CollectedCookiesSheetBridge
+- (id)initWithCollectedCookiesMac:(CollectedCookiesMac*)collectedCookies {
+ if ((self = [super init])) {
+ collectedCookies_ = collectedCookies;
+ }
+ return self;
+}
+
+- (void)sheetDidEnd:(NSWindow*)sheet
+ returnCode:(int)returnCode
+ contextInfo:(void*)contextInfo {
+ collectedCookies_->OnSheetDidEnd(sheet);
+}
+@end
+
+#pragma mark Constrained window delegate
+
+CollectedCookiesMac::CollectedCookiesMac(NSWindow* parent,
+ TabContents* tab_contents)
+ : ConstrainedWindowMacDelegateCustomSheet(
+ [[[CollectedCookiesSheetBridge alloc]
+ initWithCollectedCookiesMac:this] autorelease],
+ @selector(sheetDidEnd:returnCode:contextInfo:)),
+ tab_contents_(tab_contents) {
+ TabSpecificContentSettings* content_settings =
+ tab_contents->GetTabSpecificContentSettings();
+ registrar_.Add(this, NotificationType::COLLECTED_COOKIES_SHOWN,
+ Source<TabSpecificContentSettings>(content_settings));
+
+ sheet_controller_ = [[CollectedCookiesWindowController alloc]
+ initWithTabContents:tab_contents];
+
+ set_sheet([sheet_controller_ window]);
+
+ window_ = tab_contents->CreateConstrainedDialog(this);
+}
+
+CollectedCookiesMac::~CollectedCookiesMac() {
+ NSWindow* window = [sheet_controller_ window];
+ if (window_ && window && is_sheet_open()) {
+ window_ = NULL;
+ [NSApp endSheet:window];
+ }
+}
+
+void CollectedCookiesMac::DeleteDelegate() {
+ delete this;
+}
+
+void CollectedCookiesMac::Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ DCHECK(type == NotificationType::COLLECTED_COOKIES_SHOWN);
+ DCHECK_EQ(Source<TabSpecificContentSettings>(source).ptr(),
+ tab_contents_->GetTabSpecificContentSettings());
+ window_->CloseConstrainedWindow();
+}
+
+void CollectedCookiesMac::OnSheetDidEnd(NSWindow* sheet) {
+ [sheet orderOut:sheet_controller_];
+ if (window_)
+ window_->CloseConstrainedWindow();
+}
+
+#pragma mark Window Controller
+
+@interface CollectedCookiesWindowController(Private)
+-(void)showInfoBarForDomain:(const string16&)domain
+ setting:(ContentSetting)setting;
+-(void)showInfoBarForMultipleDomainsAndSetting:(ContentSetting)setting;
+-(void)animateInfoBar;
+@end
+
+@implementation CollectedCookiesWindowController
+
+@synthesize allowedCookiesButtonsEnabled =
+ allowedCookiesButtonsEnabled_;
+@synthesize blockedCookiesButtonsEnabled =
+ blockedCookiesButtonsEnabled_;
+
+@synthesize allowedTreeController = allowedTreeController_;
+@synthesize blockedTreeController = blockedTreeController_;
+
+- (id)initWithTabContents:(TabContents*)tabContents {
+ DCHECK(tabContents);
+ infoBarVisible_ = NO;
+ tabContents_ = tabContents;
+
+ NSString* nibpath =
+ [mac_util::MainAppBundle() pathForResource:@"CollectedCookies"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ [self loadTreeModelFromTabContents];
+
+ animation_.reset([[NSViewAnimation alloc] init]);
+ [animation_ setAnimationBlockingMode:NSAnimationNonblocking];
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ NSImage* infoIcon = rb.GetNativeImageNamed(IDR_INFO);
+ DCHECK(infoIcon);
+ [infoBarIcon_ setImage:infoIcon];
+
+ // Initialize the banner gradient and stroke color.
+ NSColor* bannerStartingColor =
+ [NSColor colorWithCalibratedRed:kBannerGradientColorTop[0]
+ green:kBannerGradientColorTop[1]
+ blue:kBannerGradientColorTop[2]
+ alpha:1.0];
+ NSColor* bannerEndingColor =
+ [NSColor colorWithCalibratedRed:kBannerGradientColorBottom[0]
+ green:kBannerGradientColorBottom[1]
+ blue:kBannerGradientColorBottom[2]
+ alpha:1.0];
+ scoped_nsobject<NSGradient> bannerGradient(
+ [[NSGradient alloc] initWithStartingColor:bannerStartingColor
+ endingColor:bannerEndingColor]);
+ [infoBar_ setGradient:bannerGradient];
+
+ NSColor* bannerStrokeColor =
+ [NSColor colorWithCalibratedWhite:kBannerStrokeColor
+ alpha:1.0];
+ [infoBar_ setStrokeColor:bannerStrokeColor];
+
+ // Change the label of the blocked cookies part if necessary.
+ if (tabContents_->profile()->GetHostContentSettingsMap()->
+ BlockThirdPartyCookies()) {
+ [blockedCookiesText_ setStringValue:l10n_util::GetNSString(
+ IDS_COLLECTED_COOKIES_BLOCKED_THIRD_PARTY_BLOCKING_ENABLED)];
+ CGFloat textDeltaY = [GTMUILocalizerAndLayoutTweaker
+ sizeToFitFixedWidthTextField:blockedCookiesText_];
+
+ // Shrink the upper custom view.
+ NSView* upperContentView = [[splitView_ subviews] objectAtIndex:0];
+ NSRect frame = [upperContentView frame];
+ [splitView_ setPosition:(frame.size.height - textDeltaY/2.0)
+ ofDividerAtIndex:0];
+
+ // Shrink the lower outline view.
+ frame = [lowerScrollView_ frame];
+ frame.size.height -= textDeltaY;
+ [lowerScrollView_ setFrame:frame];
+
+ // Move the label down so it actually fits.
+ frame = [blockedCookiesText_ frame];
+ frame.origin.y -= textDeltaY;
+ [blockedCookiesText_ setFrame:frame];
+ }
+}
+
+- (void)windowWillClose:(NSNotification*)notif {
+ [allowedOutlineView_ setDelegate:nil];
+ [blockedOutlineView_ setDelegate:nil];
+ [animation_ stopAnimation];
+ [self autorelease];
+}
+
+- (IBAction)closeSheet:(id)sender {
+ [NSApp endSheet:[self window]];
+}
+
+- (void)addException:(ContentSetting)setting
+ forTreeController:(NSTreeController*)controller {
+ NSArray* nodes = [controller selectedNodes];
+ BOOL multipleDomainsChanged = NO;
+ string16 lastDomain;
+ for (NSTreeNode* treeNode in nodes) {
+ CocoaCookieTreeNode* node = [treeNode representedObject];
+ CookieTreeNode* cookie = static_cast<CookieTreeNode*>([node treeNode]);
+ if (cookie->GetDetailedInfo().node_type !=
+ CookieTreeNode::DetailedInfo::TYPE_ORIGIN) {
+ continue;
+ }
+ CookieTreeOriginNode* origin_node =
+ static_cast<CookieTreeOriginNode*>(cookie);
+ origin_node->CreateContentException(
+ tabContents_->profile()->GetHostContentSettingsMap(),
+ setting);
+ if (!lastDomain.empty())
+ multipleDomainsChanged = YES;
+ lastDomain = origin_node->GetTitle();
+ }
+ if (multipleDomainsChanged)
+ [self showInfoBarForMultipleDomainsAndSetting:setting];
+ else
+ [self showInfoBarForDomain:lastDomain setting:setting];
+}
+
+- (IBAction)allowOrigin:(id)sender {
+ [self addException:CONTENT_SETTING_ALLOW
+ forTreeController:blockedTreeController_];
+}
+
+- (IBAction)allowForSessionFromOrigin:(id)sender {
+ [self addException:CONTENT_SETTING_SESSION_ONLY
+ forTreeController:blockedTreeController_];
+}
+
+- (IBAction)blockOrigin:(id)sender {
+ [self addException:CONTENT_SETTING_BLOCK
+ forTreeController:allowedTreeController_];
+}
+
+- (CGFloat) splitView:(NSSplitView *)sender
+ constrainMinCoordinate:(CGFloat)proposedMin
+ ofSubviewAt:(NSInteger)offset {
+ return proposedMin + kMinCollectedCookiesViewHeight;
+}
+- (CGFloat) splitView:(NSSplitView *)sender
+ constrainMaxCoordinate:(CGFloat)proposedMax
+ ofSubviewAt:(NSInteger)offset {
+ return proposedMax - kMinCollectedCookiesViewHeight;
+}
+- (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview {
+ return YES;
+}
+
+- (CocoaCookieTreeNode*)cocoaAllowedTreeModel {
+ return cocoaAllowedTreeModel_.get();
+}
+- (void)setCocoaAllowedTreeModel:(CocoaCookieTreeNode*)model {
+ cocoaAllowedTreeModel_.reset([model retain]);
+}
+
+- (CookiesTreeModel*)allowedTreeModel {
+ return allowedTreeModel_.get();
+}
+
+- (CocoaCookieTreeNode*)cocoaBlockedTreeModel {
+ return cocoaBlockedTreeModel_.get();
+}
+- (void)setCocoaBlockedTreeModel:(CocoaCookieTreeNode*)model {
+ cocoaBlockedTreeModel_.reset([model retain]);
+}
+
+- (CookiesTreeModel*)blockedTreeModel {
+ return blockedTreeModel_.get();
+}
+
+- (void)outlineView:(NSOutlineView*)outlineView
+ willDisplayCell:(id)cell
+ forTableColumn:(NSTableColumn*)tableColumn
+ item:(id)item {
+ CocoaCookieTreeNode* node = [item representedObject];
+ int index;
+ if (outlineView == allowedOutlineView_)
+ index = allowedTreeModel_->GetIconIndex([node treeNode]);
+ else
+ index = blockedTreeModel_->GetIconIndex([node treeNode]);
+ NSImage* icon = nil;
+ if (index >= 0)
+ icon = [icons_ objectAtIndex:index];
+ else
+ icon = [icons_ lastObject];
+ DCHECK([cell isKindOfClass:[ImageAndTextCell class]]);
+ [static_cast<ImageAndTextCell*>(cell) setImage:icon];
+}
+
+- (void)outlineViewSelectionDidChange:(NSNotification*)notif {
+ BOOL isAllowedOutlineView;
+ if ([notif object] == allowedOutlineView_) {
+ isAllowedOutlineView = YES;
+ } else if ([notif object] == blockedOutlineView_) {
+ isAllowedOutlineView = NO;
+ } else {
+ NOTREACHED();
+ return;
+ }
+ NSTreeController* controller =
+ isAllowedOutlineView ? allowedTreeController_ : blockedTreeController_;
+
+ NSArray* nodes = [controller selectedNodes];
+ for (NSTreeNode* treeNode in nodes) {
+ CocoaCookieTreeNode* node = [treeNode representedObject];
+ CookieTreeNode* cookie = static_cast<CookieTreeNode*>([node treeNode]);
+ if (cookie->GetDetailedInfo().node_type !=
+ CookieTreeNode::DetailedInfo::TYPE_ORIGIN) {
+ continue;
+ }
+ CookieTreeOriginNode* origin_node =
+ static_cast<CookieTreeOriginNode*>(cookie);
+ if (origin_node->CanCreateContentException()) {
+ if (isAllowedOutlineView) {
+ [self setAllowedCookiesButtonsEnabled:YES];
+ } else {
+ [self setBlockedCookiesButtonsEnabled:YES];
+ }
+ return;
+ }
+ }
+ if (isAllowedOutlineView) {
+ [self setAllowedCookiesButtonsEnabled:NO];
+ } else {
+ [self setBlockedCookiesButtonsEnabled:NO];
+ }
+}
+
+// Initializes the |allowedTreeModel_| and |blockedTreeModel_|, and builds
+// the |cocoaAllowedTreeModel_| and |cocoaBlockedTreeModel_|.
+- (void)loadTreeModelFromTabContents {
+ TabSpecificContentSettings* content_settings =
+ tabContents_->GetTabSpecificContentSettings();
+ allowedTreeModel_.reset(content_settings->GetAllowedCookiesTreeModel());
+ blockedTreeModel_.reset(content_settings->GetBlockedCookiesTreeModel());
+
+ // Convert the model's icons from Skia to Cocoa.
+ std::vector<SkBitmap> skiaIcons;
+ allowedTreeModel_->GetIcons(&skiaIcons);
+ icons_.reset([[NSMutableArray alloc] init]);
+ for (std::vector<SkBitmap>::iterator it = skiaIcons.begin();
+ it != skiaIcons.end(); ++it) {
+ [icons_ addObject:gfx::SkBitmapToNSImage(*it)];
+ }
+
+ // Default icon will be the last item in the array.
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ // TODO(rsesek): Rename this resource now that it's in multiple places.
+ [icons_ addObject:rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER)];
+
+ // Create the Cocoa model.
+ CookieTreeNode* root =
+ static_cast<CookieTreeNode*>(allowedTreeModel_->GetRoot());
+ scoped_nsobject<CocoaCookieTreeNode> model(
+ [[CocoaCookieTreeNode alloc] initWithNode:root]);
+ [self setCocoaAllowedTreeModel:model.get()]; // Takes ownership.
+ root = static_cast<CookieTreeNode*>(blockedTreeModel_->GetRoot());
+ model.reset(
+ [[CocoaCookieTreeNode alloc] initWithNode:root]);
+ [self setCocoaBlockedTreeModel:model.get()]; // Takes ownership.
+}
+
+-(void)showInfoBarForMultipleDomainsAndSetting:(ContentSetting)setting {
+ NSString* label;
+ switch (setting) {
+ case CONTENT_SETTING_BLOCK:
+ label = l10n_util::GetNSString(
+ IDS_COLLECTED_COOKIES_MULTIPLE_BLOCK_RULES_CREATED);
+ break;
+
+ case CONTENT_SETTING_ALLOW:
+ label = l10n_util::GetNSString(
+ IDS_COLLECTED_COOKIES_MULTIPLE_ALLOW_RULES_CREATED);
+ break;
+
+ case CONTENT_SETTING_SESSION_ONLY:
+ label = l10n_util::GetNSString(
+ IDS_COLLECTED_COOKIES_MULTIPLE_SESSION_RULES_CREATED);
+ break;
+
+ default:
+ NOTREACHED();
+ label = [[[NSString alloc] init] autorelease];
+ }
+ [infoBarText_ setStringValue:label];
+ [self animateInfoBar];
+}
+
+-(void)showInfoBarForDomain:(const string16&)domain
+ setting:(ContentSetting)setting {
+ NSString* label;
+ switch (setting) {
+ case CONTENT_SETTING_BLOCK:
+ label = l10n_util::GetNSStringF(
+ IDS_COLLECTED_COOKIES_BLOCK_RULE_CREATED,
+ domain);
+ break;
+
+ case CONTENT_SETTING_ALLOW:
+ label = l10n_util::GetNSStringF(
+ IDS_COLLECTED_COOKIES_ALLOW_RULE_CREATED,
+ domain);
+ break;
+
+ case CONTENT_SETTING_SESSION_ONLY:
+ label = l10n_util::GetNSStringF(
+ IDS_COLLECTED_COOKIES_SESSION_RULE_CREATED,
+ domain);
+ break;
+
+ default:
+ NOTREACHED();
+ label = [[[NSString alloc] init] autorelease];
+ }
+ [infoBarText_ setStringValue:label];
+ [self animateInfoBar];
+}
+
+-(void)animateInfoBar {
+ if (infoBarVisible_)
+ return;
+
+ infoBarVisible_ = YES;
+
+ NSMutableArray* animations = [NSMutableArray arrayWithCapacity:3];
+
+ NSWindow* sheet = [self window];
+ NSRect sheetFrame = [sheet frame];
+ NSRect infoBarFrame = [infoBar_ frame];
+ NSRect splitViewFrame = [splitView_ frame];
+
+ // Calculate the end position of the info bar and set it to its start
+ // position.
+ infoBarFrame.origin.y = NSHeight(sheetFrame);
+ infoBarFrame.size.width = NSWidth(sheetFrame);
+ NSRect infoBarStartFrame = infoBarFrame;
+ infoBarStartFrame.origin.y += NSHeight(infoBarFrame);
+ infoBarStartFrame.size.height = 0.0;
+ [infoBar_ setFrame:infoBarStartFrame];
+ [[[self window] contentView] addSubview:infoBar_];
+
+ // Calculate the new position of the sheet.
+ sheetFrame.origin.y -= NSHeight(infoBarFrame);
+ sheetFrame.size.height += NSHeight(infoBarFrame);
+
+ // Slide the infobar in.
+ [animations addObject:
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ infoBar_, NSViewAnimationTargetKey,
+ [NSValue valueWithRect:infoBarFrame],
+ NSViewAnimationEndFrameKey,
+ [NSValue valueWithRect:infoBarStartFrame],
+ NSViewAnimationStartFrameKey,
+ nil]];
+ // Make sure the split view ends up in the right position.
+ [animations addObject:
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ splitView_, NSViewAnimationTargetKey,
+ [NSValue valueWithRect:splitViewFrame],
+ NSViewAnimationEndFrameKey,
+ nil]];
+
+ // Grow the sheet.
+ [animations addObject:
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ sheet, NSViewAnimationTargetKey,
+ [NSValue valueWithRect:sheetFrame],
+ NSViewAnimationEndFrameKey,
+ nil]];
+ [animation_ setViewAnimations:animations];
+ // The default duration is 0.5s, which actually feels slow in here, so speed
+ // it up a bit.
+ [animation_ gtm_setDuration:0.2
+ eventMask:NSLeftMouseUpMask];
+ [animation_ startAnimation];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/collected_cookies_mac_unittest.mm b/chrome/browser/ui/cocoa/collected_cookies_mac_unittest.mm
new file mode 100644
index 0000000..f04c134
--- /dev/null
+++ b/chrome/browser/ui/cocoa/collected_cookies_mac_unittest.mm
@@ -0,0 +1,38 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/ref_counted.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/renderer_host/site_instance.h"
+#include "chrome/browser/renderer_host/test/test_render_view_host.h"
+#include "chrome/browser/tab_contents/test_tab_contents.h"
+#import "chrome/browser/ui/cocoa/collected_cookies_mac.h"
+#include "chrome/test/testing_profile.h"
+
+namespace {
+
+class CollectedCookiesWindowControllerTest : public RenderViewHostTestHarness {
+};
+
+TEST_F(CollectedCookiesWindowControllerTest, Construction) {
+ BrowserThread ui_thread(BrowserThread::UI, MessageLoop::current());
+ // Create a test tab. SiteInstance will be deleted when tabContents is
+ // deleted.
+ SiteInstance* instance =
+ SiteInstance::CreateSiteInstance(profile_.get());
+ TestTabContents* tabContents = new TestTabContents(profile_.get(),
+ instance);
+ CollectedCookiesWindowController* controller =
+ [[CollectedCookiesWindowController alloc]
+ initWithTabContents:tabContents];
+
+ [controller release];
+
+ delete tabContents;
+}
+
+} // namespace
+
diff --git a/chrome/browser/ui/cocoa/command_observer_bridge.h b/chrome/browser/ui/cocoa/command_observer_bridge.h
new file mode 100644
index 0000000..74179dd
--- /dev/null
+++ b/chrome/browser/ui/cocoa/command_observer_bridge.h
@@ -0,0 +1,47 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_COMMAND_OBSERVER_BRIDGE
+#define CHROME_BROWSER_UI_COCOA_COMMAND_OBSERVER_BRIDGE
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/command_updater.h"
+
+@protocol CommandObserverProtocol;
+
+// A C++ bridge class that handles listening for updates to commands and
+// passing them back to an object that supports the protocol delcared below.
+// The observer will create one of these bridges, call ObserveCommand() on the
+// command ids it cares about, and then wait for update notifications,
+// delivered via -enabledStateChangedForCommand:enabled:. Destroying this
+// bridge will handle automatically unregistering for updates, so there's no
+// need to do that manually.
+
+class CommandObserverBridge : public CommandUpdater::CommandObserver {
+ public:
+ CommandObserverBridge(id<CommandObserverProtocol> observer,
+ CommandUpdater* commands);
+ virtual ~CommandObserverBridge();
+
+ // Register for updates about |command|.
+ void ObserveCommand(int command);
+
+ protected:
+ // Overridden from CommandUpdater::CommandObserver
+ virtual void EnabledStateChangedForCommand(int command, bool enabled);
+
+ private:
+ id<CommandObserverProtocol> observer_; // weak, owns me
+ CommandUpdater* commands_; // weak
+};
+
+// Implemented by the observing Objective-C object, called when there is a
+// state change for the given command.
+@protocol CommandObserverProtocol
+- (void)enabledStateChangedForCommand:(NSInteger)command enabled:(BOOL)enabled;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_COMMAND_OBSERVER_BRIDGE
diff --git a/chrome/browser/ui/cocoa/command_observer_bridge.mm b/chrome/browser/ui/cocoa/command_observer_bridge.mm
new file mode 100644
index 0000000..0ffea97
--- /dev/null
+++ b/chrome/browser/ui/cocoa/command_observer_bridge.mm
@@ -0,0 +1,28 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/command_observer_bridge.h"
+
+#include "base/logging.h"
+
+CommandObserverBridge::CommandObserverBridge(
+ id<CommandObserverProtocol> observer, CommandUpdater* commands)
+ : observer_(observer), commands_(commands) {
+ DCHECK(observer_ && commands_);
+}
+
+CommandObserverBridge::~CommandObserverBridge() {
+ // Unregister the notifications
+ commands_->RemoveCommandObserver(this);
+}
+
+void CommandObserverBridge::ObserveCommand(int command) {
+ commands_->AddCommandObserver(command, this);
+}
+
+void CommandObserverBridge::EnabledStateChangedForCommand(int command,
+ bool enabled) {
+ [observer_ enabledStateChangedForCommand:command
+ enabled:enabled ? YES : NO];
+}
diff --git a/chrome/browser/ui/cocoa/command_observer_bridge_unittest.mm b/chrome/browser/ui/cocoa/command_observer_bridge_unittest.mm
new file mode 100644
index 0000000..371bb2c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/command_observer_bridge_unittest.mm
@@ -0,0 +1,89 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_ptr.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/command_observer_bridge.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+// Implements the callback interface. Records the last command id and
+// enabled state it has received so it can be queried by the tests to see
+// if we got a notification or not.
+@interface CommandTestObserver : NSObject<CommandObserverProtocol> {
+ @private
+ int lastCommand_; // id of last received state change
+ bool lastState_; // state of last received state change
+}
+- (int)lastCommand;
+- (bool)lastState;
+@end
+
+@implementation CommandTestObserver
+- (void)enabledStateChangedForCommand:(NSInteger)command enabled:(BOOL)enabled {
+ lastCommand_ = command;
+ lastState_ = enabled;
+}
+- (int)lastCommand {
+ return lastCommand_;
+}
+- (bool)lastState {
+ return lastState_;
+}
+@end
+
+namespace {
+
+class CommandObserverBridgeTest : public PlatformTest {
+ public:
+ CommandObserverBridgeTest()
+ : updater_(new CommandUpdater(NULL)),
+ observer_([[CommandTestObserver alloc] init]) {
+ }
+ scoped_ptr<CommandUpdater> updater_;
+ scoped_nsobject<CommandTestObserver> observer_;
+};
+
+// Tests creation and deletion. NULL arguments aren't allowed.
+TEST_F(CommandObserverBridgeTest, Create) {
+ CommandObserverBridge bridge(observer_.get(), updater_.get());
+}
+
+// Observes state changes on command ids 1 and 2. Ensure we don't get
+// a notification of a state change on a command we're not observing (3).
+// Commands start off enabled in CommandUpdater.
+TEST_F(CommandObserverBridgeTest, Observe) {
+ CommandObserverBridge bridge(observer_.get(), updater_.get());
+ bridge.ObserveCommand(1);
+ bridge.ObserveCommand(2);
+
+ // Validate initial state assumptions.
+ EXPECT_EQ([observer_ lastCommand], 0);
+ EXPECT_EQ([observer_ lastState], false);
+ EXPECT_EQ(updater_->IsCommandEnabled(1), true);
+ EXPECT_EQ(updater_->IsCommandEnabled(2), true);
+
+ updater_->UpdateCommandEnabled(1, false);
+ EXPECT_EQ([observer_ lastCommand], 1);
+ EXPECT_EQ([observer_ lastState], false);
+
+ updater_->UpdateCommandEnabled(2, false);
+ EXPECT_EQ([observer_ lastCommand], 2);
+ EXPECT_EQ([observer_ lastState], false);
+
+ updater_->UpdateCommandEnabled(1, true);
+ EXPECT_EQ([observer_ lastCommand], 1);
+ EXPECT_EQ([observer_ lastState], true);
+
+ // Change something we're not watching and make sure the last state hasn't
+ // changed.
+ updater_->UpdateCommandEnabled(3, false);
+ EXPECT_EQ([observer_ lastCommand], 1);
+ EXPECT_NE([observer_ lastCommand], 3);
+ EXPECT_EQ([observer_ lastState], true);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/confirm_quit_panel_controller.h b/chrome/browser/ui/cocoa/confirm_quit_panel_controller.h
new file mode 100644
index 0000000..fccafa9f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/confirm_quit_panel_controller.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>
+
+#include "base/cocoa_protocols_mac.h"
+
+// The ConfirmQuitPanelController manages the black HUD window that tells users
+// to "Hold Cmd+Q to Quit".
+@interface ConfirmQuitPanelController : NSWindowController<NSWindowDelegate> {
+}
+
+// Returns a singleton instance of the Controller. This will create one if it
+// does not currently exist.
++ (ConfirmQuitPanelController*)sharedController;
+
+// Shows the window.
+- (void)showWindow:(id)sender;
+
+// If the user did not confirm quit, send this message to give the user
+// instructions on how to quit.
+- (void)dismissPanel;
+
+@end
diff --git a/chrome/browser/ui/cocoa/confirm_quit_panel_controller.mm b/chrome/browser/ui/cocoa/confirm_quit_panel_controller.mm
new file mode 100644
index 0000000..548b83a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/confirm_quit_panel_controller.mm
@@ -0,0 +1,85 @@
+// 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 <QuartzCore/QuartzCore.h>
+
+#include "base/logging.h"
+#include "base/mac_util.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/confirm_quit_panel_controller.h"
+
+@interface ConfirmQuitPanelController (Private)
+- (id)initInternal;
+- (void)animateFadeOut;
+@end
+
+ConfirmQuitPanelController* g_confirmQuitPanelController = nil;
+
+@implementation ConfirmQuitPanelController
+
++ (ConfirmQuitPanelController*)sharedController {
+ if (!g_confirmQuitPanelController) {
+ g_confirmQuitPanelController =
+ [[ConfirmQuitPanelController alloc] initInternal];
+ }
+ return g_confirmQuitPanelController;
+}
+
+- (id)initInternal {
+ NSString* nibPath =
+ [mac_util::MainAppBundle() pathForResource:@"ConfirmQuitPanel"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ DCHECK([self window]);
+ DCHECK_EQ(self, [[self window] delegate]);
+}
+
+- (void)windowWillClose:(NSNotification*)notif {
+ // Release all animations because CAAnimation retains its delegate (self),
+ // which will cause a retain cycle. Break it!
+ [[self window] setAnimations:[NSDictionary dictionary]];
+ g_confirmQuitPanelController = nil;
+ [self autorelease];
+}
+
+- (void)showWindow:(id)sender {
+ // If a panel that is fading out is going to be reused here, make sure it
+ // does not get released when the animation finishes.
+ scoped_nsobject<ConfirmQuitPanelController> stayAlive([self retain]);
+ [[self window] setAnimations:[NSDictionary dictionary]];
+ [[self window] center];
+ [[self window] setAlphaValue:1.0];
+ [super showWindow:sender];
+}
+
+- (void)dismissPanel {
+ [self performSelector:@selector(animateFadeOut)
+ withObject:nil
+ afterDelay:1.0];
+}
+
+- (void)animateFadeOut {
+ NSWindow* window = [self window];
+ scoped_nsobject<CAAnimation> animation(
+ [[window animationForKey:@"alphaValue"] copy]);
+ [animation setDelegate:self];
+ [animation setDuration:0.2];
+ NSMutableDictionary* dictionary =
+ [NSMutableDictionary dictionaryWithDictionary:[window animations]];
+ [dictionary setObject:animation forKey:@"alphaValue"];
+ [window setAnimations:dictionary];
+ [[window animator] setAlphaValue:0.0];
+}
+
+- (void)animationDidStop:(CAAnimation*)theAnimation finished:(BOOL)finished {
+ [self close];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/confirm_quit_panel_controller_unittest.mm b/chrome/browser/ui/cocoa/confirm_quit_panel_controller_unittest.mm
new file mode 100644
index 0000000..0426149
--- /dev/null
+++ b/chrome/browser/ui/cocoa/confirm_quit_panel_controller_unittest.mm
@@ -0,0 +1,27 @@
+// 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/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/confirm_quit_panel_controller.h"
+
+namespace {
+
+class ConfirmQuitPanelControllerTest : public CocoaTest {
+};
+
+
+TEST_F(ConfirmQuitPanelControllerTest, ShowAndDismiss) {
+ ConfirmQuitPanelController* controller =
+ [ConfirmQuitPanelController sharedController];
+ // Test singleton.
+ EXPECT_EQ(controller, [ConfirmQuitPanelController sharedController]);
+ [controller showWindow:nil];
+ [controller dismissPanel]; // Releases self.
+ // The controller should still be the singleton instance until after the
+ // animation runs and the window closes. That will happen after this test body
+ // finishes executing.
+ EXPECT_EQ(controller, [ConfirmQuitPanelController sharedController]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/constrained_html_delegate_mac.mm b/chrome/browser/ui/cocoa/constrained_html_delegate_mac.mm
new file mode 100644
index 0000000..8d5b024
--- /dev/null
+++ b/chrome/browser/ui/cocoa/constrained_html_delegate_mac.mm
@@ -0,0 +1,153 @@
+// 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 "chrome/browser/dom_ui/constrained_html_ui.h"
+
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/dom_ui/html_dialog_ui.h"
+#include "chrome/browser/dom_ui/html_dialog_tab_contents_delegate.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/ui/cocoa/constrained_window_mac.h"
+#import <Cocoa/Cocoa.h>
+#include "ipc/ipc_message.h"
+
+class ConstrainedHtmlDelegateMac :
+ public ConstrainedWindowMacDelegateCustomSheet,
+ public HtmlDialogTabContentsDelegate,
+ public ConstrainedHtmlUIDelegate {
+
+ public:
+ ConstrainedHtmlDelegateMac(Profile* profile,
+ HtmlDialogUIDelegate* delegate);
+ ~ConstrainedHtmlDelegateMac() {}
+
+ // ConstrainedWindowMacDelegateCustomSheet -----------------------------------
+ virtual void DeleteDelegate() {
+ // From ConstrainedWindowMacDelegate: "you MUST close the sheet belonging to
+ // your delegate in this method."
+ if (is_sheet_open())
+ [NSApp endSheet:sheet()];
+ html_delegate_->OnDialogClosed("");
+ delete this;
+ }
+
+ // ConstrainedHtmlDelegate ---------------------------------------------------
+ virtual HtmlDialogUIDelegate* GetHtmlDialogUIDelegate();
+ virtual void OnDialogClose();
+
+ // HtmlDialogTabContentsDelegate ---------------------------------------------
+ void MoveContents(TabContents* source, const gfx::Rect& pos) {}
+ void ToolbarSizeChanged(TabContents* source, bool is_animating) {}
+ void HandleKeyboardEvent(const NativeWebKeyboardEvent& event) {}
+
+ void set_window(ConstrainedWindow* window) {
+ constrained_window_ = window;
+ }
+
+ private:
+ TabContents tab_contents_; // Holds the HTML to be displayed in the sheet.
+ HtmlDialogUIDelegate* html_delegate_; // weak.
+
+ // The constrained window that owns |this|. Saved here because it needs to be
+ // closed in response to the DOMUI OnDialogClose callback.
+ ConstrainedWindow* constrained_window_;
+
+ DISALLOW_COPY_AND_ASSIGN(ConstrainedHtmlDelegateMac);
+};
+
+// The delegate used to forward events from the sheet to the constrained
+// window delegate. This bridge needs to be passed into the customsheet
+// to allow the HtmlDialog to know when the sheet closes.
+@interface ConstrainedHtmlDialogSheetCocoa : NSObject {
+ ConstrainedHtmlDelegateMac* constrainedHtmlDelegate_; // weak
+}
+- (id)initWithConstrainedHtmlDelegateMac:
+ (ConstrainedHtmlDelegateMac*)ConstrainedHtmlDelegateMac;
+- (void)sheetDidEnd:(NSWindow*)sheet
+ returnCode:(int)returnCode
+ contextInfo:(void*)contextInfo;
+@end
+
+ConstrainedHtmlDelegateMac::ConstrainedHtmlDelegateMac(
+ Profile* profile,
+ HtmlDialogUIDelegate* delegate)
+ : HtmlDialogTabContentsDelegate(profile),
+ tab_contents_(profile, NULL, MSG_ROUTING_NONE, NULL, NULL),
+ html_delegate_(delegate),
+ constrained_window_(NULL) {
+ tab_contents_.set_delegate(this);
+
+ // Set |this| as a property on the tab contents so that the ConstrainedHtmlUI
+ // can get a reference to |this|.
+ ConstrainedHtmlUI::GetPropertyAccessor().SetProperty(
+ tab_contents_.property_bag(), this);
+
+ tab_contents_.controller().LoadURL(delegate->GetDialogContentURL(),
+ GURL(), PageTransition::START_PAGE);
+
+ // Create NSWindow to hold tab_contents in the constrained sheet:
+ gfx::Size size;
+ delegate->GetDialogSize(&size);
+ NSRect frame = NSMakeRect(0, 0, size.width(), size.height());
+
+ // |window| is retained by the ConstrainedWindowMacDelegateCustomSheet when
+ // the sheet is initialized.
+ scoped_nsobject<NSWindow> window;
+ window.reset(
+ [[NSWindow alloc] initWithContentRect:frame
+ styleMask:NSTitledWindowMask
+ backing:NSBackingStoreBuffered
+ defer:YES]);
+
+ [window.get() setContentView:tab_contents_.GetNativeView()];
+
+ // Set the custom sheet to point to the new window.
+ ConstrainedWindowMacDelegateCustomSheet::init(
+ window.get(),
+ [[[ConstrainedHtmlDialogSheetCocoa alloc]
+ initWithConstrainedHtmlDelegateMac:this] autorelease],
+ @selector(sheetDidEnd:returnCode:contextInfo:));
+}
+
+HtmlDialogUIDelegate* ConstrainedHtmlDelegateMac::GetHtmlDialogUIDelegate() {
+ return html_delegate_;
+}
+
+void ConstrainedHtmlDelegateMac::OnDialogClose() {
+ DCHECK(constrained_window_);
+ if (constrained_window_)
+ constrained_window_->CloseConstrainedWindow();
+}
+
+// static
+void ConstrainedHtmlUI::CreateConstrainedHtmlDialog(
+ Profile* profile,
+ HtmlDialogUIDelegate* delegate,
+ TabContents* overshadowed) {
+ // Deleted when ConstrainedHtmlDelegateMac::DeleteDelegate() runs.
+ ConstrainedHtmlDelegateMac* constrained_delegate =
+ new ConstrainedHtmlDelegateMac(profile, delegate);
+ // Deleted when ConstrainedHtmlDelegateMac::OnDialogClose() runs.
+ ConstrainedWindow* constrained_window =
+ overshadowed->CreateConstrainedDialog(constrained_delegate);
+ constrained_delegate->set_window(constrained_window);
+}
+
+@implementation ConstrainedHtmlDialogSheetCocoa
+
+- (id)initWithConstrainedHtmlDelegateMac:
+ (ConstrainedHtmlDelegateMac*)ConstrainedHtmlDelegateMac {
+ if ((self = [super init]))
+ constrainedHtmlDelegate_ = ConstrainedHtmlDelegateMac;
+ return self;
+}
+
+- (void)sheetDidEnd:(NSWindow*)sheet
+ returnCode:(int)returnCode
+ contextInfo:(void *)contextInfo {
+ [sheet orderOut:self];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/constrained_window_mac.h b/chrome/browser/ui/cocoa/constrained_window_mac.h
new file mode 100644
index 0000000..7c056ed
--- /dev/null
+++ b/chrome/browser/ui/cocoa/constrained_window_mac.h
@@ -0,0 +1,165 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_CONSTRAINED_WINDOW_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_CONSTRAINED_WINDOW_MAC_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/tab_contents/constrained_window.h"
+
+#include "base/basictypes.h"
+#include "base/logging.h"
+#include "base/scoped_nsobject.h"
+
+@class BrowserWindowController;
+@class GTMWindowSheetController;
+@class NSView;
+@class NSWindow;
+class TabContents;
+
+// Base class for constrained dialog delegates. Never inherit from this
+// directly.
+class ConstrainedWindowMacDelegate {
+ public:
+ ConstrainedWindowMacDelegate() : is_sheet_open_(false) { }
+ virtual ~ConstrainedWindowMacDelegate();
+
+ // Tells the delegate to either delete itself or set up a task to delete
+ // itself later. Note that you MUST close the sheet belonging to your delegate
+ // in this method.
+ virtual void DeleteDelegate() = 0;
+
+ // Called by the tab controller, you do not need to do anything yourself
+ // with this method.
+ virtual void RunSheet(GTMWindowSheetController* sheetController,
+ NSView* view) = 0;
+ protected:
+ // Returns true if this delegate's sheet is currently showing.
+ bool is_sheet_open() { return is_sheet_open_; }
+
+ private:
+ bool is_sheet_open_;
+ void set_sheet_open(bool is_open) { is_sheet_open_ = is_open; }
+ friend class ConstrainedWindowMac;
+};
+
+// Subclass this for a dialog delegate that displays a system sheet such as
+// an NSAlert, an open or save file panel, etc.
+class ConstrainedWindowMacDelegateSystemSheet
+ : public ConstrainedWindowMacDelegate {
+ public:
+ ConstrainedWindowMacDelegateSystemSheet(id delegate, SEL didEndSelector)
+ : systemSheet_(nil),
+ delegate_([delegate retain]),
+ didEndSelector_(didEndSelector) { }
+
+ protected:
+ void set_sheet(id sheet) { systemSheet_.reset([sheet retain]); }
+ id sheet() { return systemSheet_; }
+
+ // Returns an NSArray to be passed as parameters to GTMWindowSheetController.
+ // Array's contents should be the arguments passed to the system sheet's
+ // beginSheetForWindow:... method. The window argument must be [NSNull null].
+ //
+ // The default implementation returns
+ // [null window, delegate, didEndSelector, null contextInfo]
+ // Subclasses may override this if they show a system sheet which takes
+ // different parameters.
+ virtual NSArray* GetSheetParameters(id delegate, SEL didEndSelector);
+
+ private:
+ virtual void RunSheet(GTMWindowSheetController* sheetController,
+ NSView* view);
+ scoped_nsobject<id> systemSheet_;
+ scoped_nsobject<id> delegate_;
+ SEL didEndSelector_;
+};
+
+// Subclass this for a dialog delegate that displays a custom sheet, e.g. loaded
+// from a nib file.
+class ConstrainedWindowMacDelegateCustomSheet
+ : public ConstrainedWindowMacDelegate {
+ public:
+ ConstrainedWindowMacDelegateCustomSheet()
+ : customSheet_(nil),
+ delegate_(nil),
+ didEndSelector_(NULL) { }
+
+ ConstrainedWindowMacDelegateCustomSheet(id delegate, SEL didEndSelector)
+ : customSheet_(nil),
+ delegate_([delegate retain]),
+ didEndSelector_(didEndSelector) { }
+
+ protected:
+ // For when you need to delay initalization after the constructor call.
+ void init(NSWindow* sheet, id delegate, SEL didEndSelector) {
+ DCHECK(!delegate_.get());
+ DCHECK(!didEndSelector_);
+ customSheet_.reset([sheet retain]);
+ delegate_.reset([delegate retain]);
+ didEndSelector_ = didEndSelector;
+ DCHECK(delegate_.get());
+ DCHECK(didEndSelector_);
+ }
+ void set_sheet(NSWindow* sheet) { customSheet_.reset([sheet retain]); }
+ NSWindow* sheet() { return customSheet_; }
+
+ private:
+ virtual void RunSheet(GTMWindowSheetController* sheetController,
+ NSView* view);
+ scoped_nsobject<NSWindow> customSheet_;
+ scoped_nsobject<id> delegate_;
+ SEL didEndSelector_;
+};
+
+// Constrained window implementation for the Mac port. A constrained window
+// is a per-tab sheet on OS X.
+//
+// Constrained windows work slightly differently on OS X than on the other
+// platforms:
+// 1. A constrained window is bound to both a tab and window on OS X.
+// 2. The delegate is responsible for closing the sheet again when it is
+// deleted.
+class ConstrainedWindowMac : public ConstrainedWindow {
+ public:
+ virtual ~ConstrainedWindowMac();
+
+ // Overridden from ConstrainedWindow:
+ virtual void ShowConstrainedWindow();
+ virtual void CloseConstrainedWindow();
+
+ // Returns the TabContents that constrains this Constrained Window.
+ TabContents* owner() const { return owner_; }
+
+ // Returns the window's delegate.
+ ConstrainedWindowMacDelegate* delegate() { return delegate_; }
+
+ // Makes the constrained window visible, if it is not yet visible.
+ void Realize(BrowserWindowController* controller);
+
+ private:
+ friend class ConstrainedWindow;
+
+ ConstrainedWindowMac(TabContents* owner,
+ ConstrainedWindowMacDelegate* delegate);
+
+ // The TabContents that owns and constrains this ConstrainedWindow.
+ TabContents* owner_;
+
+ // Delegate that provides the contents of this constrained window.
+ ConstrainedWindowMacDelegate* delegate_;
+
+ // Controller of the window that contains this sheet.
+ BrowserWindowController* controller_;
+
+ // Stores if |ShowConstrainedWindow()| was called.
+ bool should_be_visible_;
+
+ DISALLOW_COPY_AND_ASSIGN(ConstrainedWindowMac);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_CONSTRAINED_WINDOW_MAC_H_
+
diff --git a/chrome/browser/ui/cocoa/constrained_window_mac.mm b/chrome/browser/ui/cocoa/constrained_window_mac.mm
new file mode 100644
index 0000000..9c408a0
--- /dev/null
+++ b/chrome/browser/ui/cocoa/constrained_window_mac.mm
@@ -0,0 +1,104 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/constrained_window_mac.h"
+
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents/tab_contents_view.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "third_party/GTM/AppKit/GTMWindowSheetController.h"
+
+ConstrainedWindowMacDelegate::~ConstrainedWindowMacDelegate() {}
+
+NSArray* ConstrainedWindowMacDelegateSystemSheet::GetSheetParameters(
+ id delegate,
+ SEL didEndSelector) {
+ return [NSArray arrayWithObjects:
+ [NSNull null], // window, must be [NSNull null]
+ delegate,
+ [NSValue valueWithPointer:didEndSelector],
+ [NSValue valueWithPointer:NULL], // context info for didEndSelector_.
+ nil];
+}
+
+void ConstrainedWindowMacDelegateSystemSheet::RunSheet(
+ GTMWindowSheetController* sheetController,
+ NSView* view) {
+ NSArray* params = GetSheetParameters(delegate_.get(), didEndSelector_);
+ [sheetController beginSystemSheet:systemSheet_
+ modalForView:view
+ withParameters:params];
+}
+
+void ConstrainedWindowMacDelegateCustomSheet::RunSheet(
+ GTMWindowSheetController* sheetController,
+ NSView* view) {
+ [sheetController beginSheet:customSheet_.get()
+ modalForView:view
+ modalDelegate:delegate_.get()
+ didEndSelector:didEndSelector_
+ contextInfo:NULL];
+}
+
+// static
+ConstrainedWindow* ConstrainedWindow::CreateConstrainedDialog(
+ TabContents* parent,
+ ConstrainedWindowMacDelegate* delegate) {
+ return new ConstrainedWindowMac(parent, delegate);
+}
+
+ConstrainedWindowMac::ConstrainedWindowMac(
+ TabContents* owner, ConstrainedWindowMacDelegate* delegate)
+ : owner_(owner),
+ delegate_(delegate),
+ controller_(nil),
+ should_be_visible_(false) {
+ DCHECK(owner);
+ DCHECK(delegate);
+}
+
+ConstrainedWindowMac::~ConstrainedWindowMac() {}
+
+void ConstrainedWindowMac::ShowConstrainedWindow() {
+ should_be_visible_ = true;
+ // The TabContents only has a native window if it is currently visible. In
+ // this case, open the sheet now. Else, Realize() will be called later, when
+ // our tab becomes visible.
+ NSWindow* browserWindow = owner_->view()->GetTopLevelNativeWindow();
+ NSWindowController* controller = [browserWindow windowController];
+ if (controller != nil) {
+ DCHECK([controller isKindOfClass:[BrowserWindowController class]]);
+ BrowserWindowController* browser_controller =
+ static_cast<BrowserWindowController*>(controller);
+ if ([browser_controller canAttachConstrainedWindow])
+ Realize(browser_controller);
+ }
+}
+
+void ConstrainedWindowMac::CloseConstrainedWindow() {
+ // Note: controller_ can be `nil` here if the sheet was never realized. That's
+ // ok.
+ [controller_ removeConstrainedWindow:this];
+ delegate_->DeleteDelegate();
+ owner_->WillClose(this);
+
+ delete this;
+}
+
+void ConstrainedWindowMac::Realize(BrowserWindowController* controller) {
+ if (!should_be_visible_)
+ return;
+
+ if (controller_ != nil) {
+ DCHECK(controller_ == controller);
+ return;
+ }
+ DCHECK(controller != nil);
+
+ // Remember the controller we're adding ourselves to, so that we can later
+ // remove us from it.
+ controller_ = controller;
+ [controller_ attachConstrainedWindow:this];
+ delegate_->set_sheet_open(true);
+}
diff --git a/chrome/browser/ui/cocoa/content_exceptions_window_controller.h b/chrome/browser/ui/cocoa/content_exceptions_window_controller.h
new file mode 100644
index 0000000..23d1a98
--- /dev/null
+++ b/chrome/browser/ui/cocoa/content_exceptions_window_controller.h
@@ -0,0 +1,74 @@
+// 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/cocoa_protocols_mac.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/content_settings/host_content_settings_map.h"
+#include "chrome/common/content_settings_types.h"
+
+class ContentExceptionsTableModel;
+class ContentSettingComboModel;
+class UpdatingContentSettingsObserver;
+
+// Controller for the content exception dialogs.
+@interface ContentExceptionsWindowController : NSWindowController
+ <NSWindowDelegate,
+ NSTableViewDataSource,
+ NSTableViewDelegate> {
+ @private
+ IBOutlet NSTableView* tableView_;
+ IBOutlet NSButton* addButton_;
+ IBOutlet NSButton* removeButton_;
+ IBOutlet NSButton* removeAllButton_;
+ IBOutlet NSButton* doneButton_;
+
+ ContentSettingsType settingsType_;
+ HostContentSettingsMap* settingsMap_; // weak
+ HostContentSettingsMap* otrSettingsMap_; // weak
+ scoped_ptr<ContentExceptionsTableModel> model_;
+ scoped_ptr<ContentSettingComboModel> popup_model_;
+
+ // Is set if adding and editing exceptions for the current OTR session should
+ // be allowed.
+ BOOL otrAllowed_;
+
+ // Listens for changes to the content settings and reloads the data when they
+ // change. See comment in -modelDidChange in the mm file for details.
+ scoped_ptr<UpdatingContentSettingsObserver> tableObserver_;
+
+ // If this is set to NO, notifications by |tableObserver_| are ignored. This
+ // is used to suppress updates at bad times.
+ BOOL updatesEnabled_;
+
+ // This is non-NULL only while a new element is being added and its pattern
+ // is being edited.
+ scoped_ptr<HostContentSettingsMap::PatternSettingPair> newException_;
+}
+
+// Returns the content exceptions window controller for |settingsType|.
+// Changes made by the user in the window are persisted in |settingsMap|.
++ (id)controllerForType:(ContentSettingsType)settingsType
+ settingsMap:(HostContentSettingsMap*)settingsMap
+ otrSettingsMap:(HostContentSettingsMap*)otrSettingsMap;
+
+// Shows the exceptions dialog as a modal sheet attached to |window|.
+- (void)attachSheetTo:(NSWindow*)window;
+
+// Sets the minimum width of the sheet and resizes it if necessary.
+- (void)setMinWidth:(CGFloat)minWidth;
+
+- (IBAction)addException:(id)sender;
+- (IBAction)removeException:(id)sender;
+- (IBAction)removeAllExceptions:(id)sender;
+// Closes the sheet and ends the modal loop.
+- (IBAction)closeSheet:(id)sender;
+
+@end
+
+@interface ContentExceptionsWindowController(VisibleForTesting)
+- (void)cancel:(id)sender;
+- (BOOL)editingNewException;
+@end
diff --git a/chrome/browser/ui/cocoa/content_exceptions_window_controller.mm b/chrome/browser/ui/cocoa/content_exceptions_window_controller.mm
new file mode 100644
index 0000000..1a6aa74
--- /dev/null
+++ b/chrome/browser/ui/cocoa/content_exceptions_window_controller.mm
@@ -0,0 +1,490 @@
+// 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/ui/cocoa/content_exceptions_window_controller.h"
+
+#include "app/l10n_util.h"
+#include "app/l10n_util_mac.h"
+#include "app/table_model_observer.h"
+#include "base/command_line.h"
+#import "base/mac_util.h"
+#import "base/scoped_nsobject.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/content_exceptions_table_model.h"
+#include "chrome/browser/content_setting_combo_model.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/common/notification_registrar.h"
+#include "chrome/common/notification_service.h"
+#include "grit/generated_resources.h"
+#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+@interface ContentExceptionsWindowController (Private)
+- (id)initWithType:(ContentSettingsType)settingsType
+ settingsMap:(HostContentSettingsMap*)settingsMap
+ otrSettingsMap:(HostContentSettingsMap*)otrSettingsMap;
+- (void)updateRow:(NSInteger)row
+ withEntry:(const HostContentSettingsMap::PatternSettingPair&)entry
+ forOtr:(BOOL)isOtr;
+- (void)adjustEditingButtons;
+- (void)modelDidChange;
+- (NSString*)titleForIndex:(size_t)index;
+@end
+
+////////////////////////////////////////////////////////////////////////////////
+// PatternFormatter
+
+// A simple formatter that accepts text that vaguely looks like a pattern.
+@interface PatternFormatter : NSFormatter
+@end
+
+@implementation PatternFormatter
+- (NSString*)stringForObjectValue:(id)object {
+ if (![object isKindOfClass:[NSString class]])
+ return nil;
+ return object;
+}
+
+- (BOOL)getObjectValue:(id*)object
+ forString:(NSString*)string
+ errorDescription:(NSString**)error {
+ if ([string length]) {
+ if (HostContentSettingsMap::Pattern(
+ base::SysNSStringToUTF8(string)).IsValid()) {
+ *object = string;
+ return YES;
+ }
+ }
+ if (error)
+ *error = @"Invalid pattern";
+ return NO;
+}
+
+- (NSAttributedString*)attributedStringForObjectValue:(id)object
+ withDefaultAttributes:(NSDictionary*)attribs {
+ return nil;
+}
+@end
+
+////////////////////////////////////////////////////////////////////////////////
+// UpdatingContentSettingsObserver
+
+// UpdatingContentSettingsObserver is a notification observer that tells a
+// window controller to update its data on every notification.
+class UpdatingContentSettingsObserver : public NotificationObserver {
+ public:
+ UpdatingContentSettingsObserver(ContentExceptionsWindowController* controller)
+ : controller_(controller) {
+ // One would think one could register a TableModelObserver to be notified of
+ // changes to ContentExceptionsTableModel. One would be wrong: The table
+ // model only sends out changes that are made through the model, not for
+ // changes made directly to its backing HostContentSettings object (that
+ // happens e.g. if the user uses the cookie confirmation dialog). Hence,
+ // observe the CONTENT_SETTINGS_CHANGED notification directly.
+ registrar_.Add(this, NotificationType::CONTENT_SETTINGS_CHANGED,
+ NotificationService::AllSources());
+ }
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+ private:
+ NotificationRegistrar registrar_;
+ ContentExceptionsWindowController* controller_;
+};
+
+void UpdatingContentSettingsObserver::Observe(
+ NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ [controller_ modelDidChange];
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Static functions
+
+namespace {
+
+NSString* GetWindowTitle(ContentSettingsType settingsType) {
+ switch (settingsType) {
+ case CONTENT_SETTINGS_TYPE_COOKIES:
+ return l10n_util::GetNSStringWithFixup(IDS_COOKIE_EXCEPTION_TITLE);
+ case CONTENT_SETTINGS_TYPE_IMAGES:
+ return l10n_util::GetNSStringWithFixup(IDS_IMAGES_EXCEPTION_TITLE);
+ case CONTENT_SETTINGS_TYPE_JAVASCRIPT:
+ return l10n_util::GetNSStringWithFixup(IDS_JS_EXCEPTION_TITLE);
+ case CONTENT_SETTINGS_TYPE_PLUGINS:
+ return l10n_util::GetNSStringWithFixup(IDS_PLUGINS_EXCEPTION_TITLE);
+ case CONTENT_SETTINGS_TYPE_POPUPS:
+ return l10n_util::GetNSStringWithFixup(IDS_POPUP_EXCEPTION_TITLE);
+ default:
+ NOTREACHED();
+ }
+ return @"";
+}
+
+const CGFloat kButtonBarHeight = 35.0;
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+// ContentExceptionsWindowController implementation
+
+static ContentExceptionsWindowController*
+ g_exceptionWindows[CONTENT_SETTINGS_NUM_TYPES] = { nil };
+
+@implementation ContentExceptionsWindowController
+
++ (id)controllerForType:(ContentSettingsType)settingsType
+ settingsMap:(HostContentSettingsMap*)settingsMap
+ otrSettingsMap:(HostContentSettingsMap*)otrSettingsMap {
+ if (!g_exceptionWindows[settingsType]) {
+ g_exceptionWindows[settingsType] =
+ [[ContentExceptionsWindowController alloc]
+ initWithType:settingsType
+ settingsMap:settingsMap
+ otrSettingsMap:otrSettingsMap];
+ }
+ return g_exceptionWindows[settingsType];
+}
+
+- (id)initWithType:(ContentSettingsType)settingsType
+ settingsMap:(HostContentSettingsMap*)settingsMap
+ otrSettingsMap:(HostContentSettingsMap*)otrSettingsMap {
+ NSString* nibpath =
+ [mac_util::MainAppBundle() pathForResource:@"ContentExceptionsWindow"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ settingsType_ = settingsType;
+ settingsMap_ = settingsMap;
+ otrSettingsMap_ = otrSettingsMap;
+ model_.reset(new ContentExceptionsTableModel(
+ settingsMap_, otrSettingsMap_, settingsType_));
+ popup_model_.reset(new ContentSettingComboModel(settingsType_));
+ otrAllowed_ = otrSettingsMap != NULL;
+ tableObserver_.reset(new UpdatingContentSettingsObserver(self));
+ updatesEnabled_ = YES;
+
+ // TODO(thakis): autoremember window rect.
+ // TODO(thakis): sorting support.
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ DCHECK([self window]);
+ DCHECK_EQ(self, [[self window] delegate]);
+ DCHECK(tableView_);
+ DCHECK_EQ(self, [tableView_ dataSource]);
+ DCHECK_EQ(self, [tableView_ delegate]);
+
+ [[self window] setTitle:GetWindowTitle(settingsType_)];
+
+ CGFloat minWidth = [[addButton_ superview] bounds].size.width +
+ [[doneButton_ superview] bounds].size.width;
+ [self setMinWidth:minWidth];
+
+ [self adjustEditingButtons];
+
+ // Initialize menu for the data cell in the "action" column.
+ scoped_nsobject<NSMenu> menu([[NSMenu alloc] initWithTitle:@"exceptionMenu"]);
+ for (int i = 0; i < popup_model_->GetItemCount(); ++i) {
+ NSString* title =
+ l10n_util::FixUpWindowsStyleLabel(popup_model_->GetItemAt(i));
+ scoped_nsobject<NSMenuItem> allowItem(
+ [[NSMenuItem alloc] initWithTitle:title action:NULL keyEquivalent:@""]);
+ [allowItem.get() setTag:popup_model_->SettingForIndex(i)];
+ [menu.get() addItem:allowItem.get()];
+ }
+ NSCell* menuCell =
+ [[tableView_ tableColumnWithIdentifier:@"action"] dataCell];
+ [menuCell setMenu:menu.get()];
+
+ NSCell* patternCell =
+ [[tableView_ tableColumnWithIdentifier:@"pattern"] dataCell];
+ [patternCell setFormatter:[[[PatternFormatter alloc] init] autorelease]];
+
+ if (!otrAllowed_) {
+ [tableView_
+ removeTableColumn:[tableView_ tableColumnWithIdentifier:@"otr"]];
+ }
+}
+
+- (void)setMinWidth:(CGFloat)minWidth {
+ NSWindow* window = [self window];
+ [window setMinSize:NSMakeSize(minWidth, [window minSize].height)];
+ if ([window frame].size.width < minWidth) {
+ NSRect frame = [window frame];
+ frame.size.width = minWidth;
+ [window setFrame:frame display:NO];
+ }
+}
+
+- (void)windowWillClose:(NSNotification*)notification {
+ // Without this, some of the unit tests fail on 10.6:
+ [tableView_ setDataSource:nil];
+
+ g_exceptionWindows[settingsType_] = nil;
+ [self autorelease];
+}
+
+- (BOOL)editingNewException {
+ return newException_.get() != NULL;
+}
+
+// Let esc cancel editing if the user is currently editing a pattern. Else, let
+// esc close the window.
+- (void)cancel:(id)sender {
+ if ([tableView_ currentEditor] != nil) {
+ [tableView_ abortEditing];
+ [[self window] makeFirstResponder:tableView_]; // Re-gain focus.
+
+ if ([tableView_ selectedRow] == model_->RowCount()) {
+ // Cancel addition of new row.
+ [self removeException:self];
+ }
+ } else {
+ [self closeSheet:self];
+ }
+}
+
+- (void)keyDown:(NSEvent*)event {
+ NSString* chars = [event charactersIgnoringModifiers];
+ if ([chars length] == 1) {
+ switch ([chars characterAtIndex:0]) {
+ case NSDeleteCharacter:
+ case NSDeleteFunctionKey:
+ // Delete deletes.
+ if ([[tableView_ selectedRowIndexes] count] > 0)
+ [self removeException:self];
+ return;
+ case NSCarriageReturnCharacter:
+ case NSEnterCharacter:
+ // Return enters rename mode.
+ if ([[tableView_ selectedRowIndexes] count] == 1) {
+ [tableView_ editColumn:0
+ row:[[tableView_ selectedRowIndexes] lastIndex]
+ withEvent:nil
+ select:YES];
+ }
+ return;
+ }
+ }
+ [super keyDown:event];
+}
+
+- (void)attachSheetTo:(NSWindow*)window {
+ [NSApp beginSheet:[self window]
+ modalForWindow:window
+ modalDelegate:self
+ didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
+ contextInfo:nil];
+}
+
+- (void)sheetDidEnd:(NSWindow*)sheet
+ returnCode:(NSInteger)returnCode
+ contextInfo:(void*)context {
+ [sheet close];
+ [sheet orderOut:self];
+}
+
+- (IBAction)addException:(id)sender {
+ if (newException_.get()) {
+ // The invariant is that |newException_| is non-NULL exactly if the pattern
+ // of a new exception is currently being edited - so there's nothing to do
+ // in that case.
+ return;
+ }
+ newException_.reset(new HostContentSettingsMap::PatternSettingPair);
+ newException_->first = HostContentSettingsMap::Pattern(
+ l10n_util::GetStringUTF8(IDS_EXCEPTIONS_SAMPLE_PATTERN));
+ newException_->second = CONTENT_SETTING_BLOCK;
+ [tableView_ reloadData];
+
+ [self adjustEditingButtons];
+ int index = model_->RowCount();
+ NSIndexSet* selectedSet = [NSIndexSet indexSetWithIndex:index];
+ [tableView_ selectRowIndexes:selectedSet byExtendingSelection:NO];
+ [tableView_ editColumn:0 row:index withEvent:nil select:YES];
+}
+
+- (IBAction)removeException:(id)sender {
+ updatesEnabled_ = NO;
+ NSIndexSet* selection = [tableView_ selectedRowIndexes];
+ [tableView_ deselectAll:self]; // Else we'll get a -setObjectValue: later.
+ DCHECK_GT([selection count], 0U);
+ NSUInteger index = [selection lastIndex];
+ while (index != NSNotFound) {
+ if (index == static_cast<NSUInteger>(model_->RowCount()))
+ newException_.reset();
+ else
+ model_->RemoveException(index);
+ index = [selection indexLessThanIndex:index];
+ }
+ updatesEnabled_ = YES;
+ [self modelDidChange];
+}
+
+- (IBAction)removeAllExceptions:(id)sender {
+ updatesEnabled_ = NO;
+ [tableView_ deselectAll:self]; // Else we'll get a -setObjectValue: later.
+ newException_.reset();
+ model_->RemoveAll();
+ updatesEnabled_ = YES;
+ [self modelDidChange];
+}
+
+- (IBAction)closeSheet:(id)sender {
+ [NSApp endSheet:[self window]];
+}
+
+// Table View Data Source -----------------------------------------------------
+
+- (NSInteger)numberOfRowsInTableView:(NSTableView*)table {
+ return model_->RowCount() + (newException_.get() ? 1 : 0);
+}
+
+- (id)tableView:(NSTableView*)tv
+ objectValueForTableColumn:(NSTableColumn*)tableColumn
+ row:(NSInteger)row {
+ const HostContentSettingsMap::PatternSettingPair* entry;
+ int isOtr;
+ if (newException_.get() && row >= model_->RowCount()) {
+ entry = newException_.get();
+ isOtr = 0;
+ } else {
+ entry = &model_->entry_at(row);
+ isOtr = model_->entry_is_off_the_record(row) ? 1 : 0;
+ }
+
+ NSObject* result = nil;
+ NSString* identifier = [tableColumn identifier];
+ if ([identifier isEqualToString:@"pattern"]) {
+ result = base::SysUTF8ToNSString(entry->first.AsString());
+ } else if ([identifier isEqualToString:@"action"]) {
+ result =
+ [NSNumber numberWithInt:popup_model_->IndexForSetting(entry->second)];
+ } else if ([identifier isEqualToString:@"otr"]) {
+ result = [NSNumber numberWithInt:isOtr];
+ } else {
+ NOTREACHED();
+ }
+ return result;
+}
+
+// Updates exception at |row| to contain the data in |entry|.
+- (void)updateRow:(NSInteger)row
+ withEntry:(const HostContentSettingsMap::PatternSettingPair&)entry
+ forOtr:(BOOL)isOtr {
+ // TODO(thakis): This apparently moves an edited row to the back of the list.
+ // It's what windows and linux do, but it's kinda sucky. Fix.
+ // http://crbug.com/36904
+ updatesEnabled_ = NO;
+ if (row < model_->RowCount())
+ model_->RemoveException(row);
+ model_->AddException(entry.first, entry.second, isOtr);
+ updatesEnabled_ = YES;
+ [self modelDidChange];
+
+ // For now, at least re-select the edited element.
+ int newIndex = model_->IndexOfExceptionByPattern(entry.first, isOtr);
+ DCHECK(newIndex != -1);
+ [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:newIndex]
+ byExtendingSelection:NO];
+}
+
+- (void) tableView:(NSTableView*)tv
+ setObjectValue:(id)object
+ forTableColumn:(NSTableColumn*)tableColumn
+ row:(NSInteger)row {
+ // -remove: and -removeAll: both call |tableView_|'s -deselectAll:, which
+ // calls this method if a cell is currently being edited. Do not commit edits
+ // of rows that are about to be deleted.
+ if (!updatesEnabled_) {
+ // If this method gets called, the pattern filed of the new exception can no
+ // longer be being edited. Reset |newException_| to keep the invariant true.
+ newException_.reset();
+ return;
+ }
+
+ // Get model object.
+ bool isNewRow = newException_.get() && row >= model_->RowCount();
+ HostContentSettingsMap::PatternSettingPair originalEntry =
+ isNewRow ? *newException_ : model_->entry_at(row);
+ HostContentSettingsMap::PatternSettingPair entry = originalEntry;
+ bool isOtr =
+ isNewRow ? 0 : model_->entry_is_off_the_record(row);
+ bool wasOtr = isOtr;
+
+ // Modify it.
+ NSString* identifier = [tableColumn identifier];
+ if ([identifier isEqualToString:@"pattern"]) {
+ entry.first = HostContentSettingsMap::Pattern(
+ base::SysNSStringToUTF8(object));
+ }
+ if ([identifier isEqualToString:@"action"]) {
+ int index = [object intValue];
+ entry.second = popup_model_->SettingForIndex(index);
+ }
+ if ([identifier isEqualToString:@"otr"]) {
+ isOtr = [object intValue] != 0;
+ }
+
+ // Commit modification, if any.
+ if (isNewRow) {
+ newException_.reset();
+ if (![identifier isEqualToString:@"pattern"]) {
+ [tableView_ reloadData];
+ [self adjustEditingButtons];
+ return; // Commit new rows only when the pattern has been set.
+ }
+ int newIndex = model_->IndexOfExceptionByPattern(entry.first, false);
+ if (newIndex != -1) {
+ // The new pattern was already in the table. Focus existing row instead of
+ // overwriting it with a new one.
+ [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:newIndex]
+ byExtendingSelection:NO];
+ [tableView_ reloadData];
+ [self adjustEditingButtons];
+ return;
+ }
+ }
+ if (entry != originalEntry || wasOtr != isOtr || isNewRow)
+ [self updateRow:row withEntry:entry forOtr:isOtr];
+}
+
+
+// Table View Delegate --------------------------------------------------------
+
+// When the selection in the table view changes, we need to adjust buttons.
+- (void)tableViewSelectionDidChange:(NSNotification*)notification {
+ [self adjustEditingButtons];
+}
+
+// Private --------------------------------------------------------------------
+
+// This method appropriately sets the enabled states on the table's editing
+// buttons.
+- (void)adjustEditingButtons {
+ NSIndexSet* selection = [tableView_ selectedRowIndexes];
+ [removeButton_ setEnabled:([selection count] > 0)];
+ [removeAllButton_ setEnabled:([tableView_ numberOfRows] > 0)];
+}
+
+- (void)modelDidChange {
+ // Some calls on |model_|, e.g. RemoveException(), change something on the
+ // backing content settings map object (which sends a notification) and then
+ // change more stuff in |model_|. If |model_| is deleted when the notification
+ // is sent, this second access causes a segmentation violation. Hence, disable
+ // resetting |model_| while updates can be in progress.
+ if (!updatesEnabled_)
+ return;
+
+ // The model caches its data, meaning we need to recreate it on every change.
+ model_.reset(new ContentExceptionsTableModel(
+ settingsMap_, otrSettingsMap_, settingsType_));
+
+ [tableView_ reloadData];
+ [self adjustEditingButtons];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/content_exceptions_window_controller_unittest.mm b/chrome/browser/ui/cocoa/content_exceptions_window_controller_unittest.mm
new file mode 100644
index 0000000..99dfb9d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/content_exceptions_window_controller_unittest.mm
@@ -0,0 +1,252 @@
+// 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/ui/cocoa/content_exceptions_window_controller.h"
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/scoped_nsobject.h"
+#include "base/ref_counted.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+void ProcessEvents() {
+ for (;;) {
+ base::mac::ScopedNSAutoreleasePool pool;
+ NSEvent* next_event = [NSApp nextEventMatchingMask:NSAnyEventMask
+ untilDate:nil
+ inMode:NSDefaultRunLoopMode
+ dequeue:YES];
+ if (!next_event)
+ break;
+ [NSApp sendEvent:next_event];
+ }
+}
+
+void SendKeyEvents(NSString* characters) {
+ for (NSUInteger i = 0; i < [characters length]; ++i) {
+ unichar character = [characters characterAtIndex:i];
+ NSString* charString = [NSString stringWithCharacters:&character length:1];
+ NSEvent* event = [NSEvent keyEventWithType:NSKeyDown
+ location:NSZeroPoint
+ modifierFlags:0
+ timestamp:0.0
+ windowNumber:0
+ context:nil
+ characters:charString
+ charactersIgnoringModifiers:charString
+ isARepeat:NO
+ keyCode:0];
+ [NSApp sendEvent:event];
+ }
+}
+
+class ContentExceptionsWindowControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ TestingProfile* profile = browser_helper_.profile();
+ settingsMap_ = new HostContentSettingsMap(profile);
+ }
+
+ ContentExceptionsWindowController* GetController(ContentSettingsType type) {
+ id controller = [ContentExceptionsWindowController
+ controllerForType:type
+ settingsMap:settingsMap_.get()
+ otrSettingsMap:NULL];
+ [controller showWindow:nil];
+ return controller;
+ }
+
+ void ClickAdd(ContentExceptionsWindowController* controller) {
+ [controller addException:nil];
+ ProcessEvents();
+ }
+
+ void ClickRemove(ContentExceptionsWindowController* controller) {
+ [controller removeException:nil];
+ ProcessEvents();
+ }
+
+ void ClickRemoveAll(ContentExceptionsWindowController* controller) {
+ [controller removeAllExceptions:nil];
+ ProcessEvents();
+ }
+
+ void EnterText(NSString* str) {
+ SendKeyEvents(str);
+ ProcessEvents();
+ }
+
+ void HitEscape(ContentExceptionsWindowController* controller) {
+ [controller cancel:nil];
+ ProcessEvents();
+ }
+
+ protected:
+ BrowserTestHelper browser_helper_;
+ scoped_refptr<HostContentSettingsMap> settingsMap_;
+};
+
+TEST_F(ContentExceptionsWindowControllerTest, Construction) {
+ ContentExceptionsWindowController* controller =
+ [ContentExceptionsWindowController
+ controllerForType:CONTENT_SETTINGS_TYPE_PLUGINS
+ settingsMap:settingsMap_.get()
+ otrSettingsMap:NULL];
+ [controller showWindow:nil];
+ [controller close]; // Should autorelease.
+}
+
+// Regression test for http://crbug.com/37137
+TEST_F(ContentExceptionsWindowControllerTest, AddRemove) {
+ ContentExceptionsWindowController* controller =
+ GetController(CONTENT_SETTINGS_TYPE_PLUGINS);
+
+ HostContentSettingsMap::SettingsForOneType settings;
+
+ ClickAdd(controller);
+ settingsMap_->GetSettingsForOneType(CONTENT_SETTINGS_TYPE_PLUGINS,
+ "",
+ &settings);
+ EXPECT_EQ(0u, settings.size());
+
+ ClickRemove(controller);
+
+ EXPECT_FALSE([controller editingNewException]);
+ [controller close];
+
+ settingsMap_->GetSettingsForOneType(CONTENT_SETTINGS_TYPE_PLUGINS,
+ "",
+ &settings);
+ EXPECT_EQ(0u, settings.size());
+}
+
+// Regression test for http://crbug.com/37137
+TEST_F(ContentExceptionsWindowControllerTest, AddRemoveAll) {
+ ContentExceptionsWindowController* controller =
+ GetController(CONTENT_SETTINGS_TYPE_PLUGINS);
+
+ ClickAdd(controller);
+ ClickRemoveAll(controller);
+
+ EXPECT_FALSE([controller editingNewException]);
+ [controller close];
+
+ HostContentSettingsMap::SettingsForOneType settings;
+ settingsMap_->GetSettingsForOneType(CONTENT_SETTINGS_TYPE_PLUGINS,
+ "",
+ &settings);
+ EXPECT_EQ(0u, settings.size());
+}
+
+TEST_F(ContentExceptionsWindowControllerTest, Add) {
+ ContentExceptionsWindowController* controller =
+ GetController(CONTENT_SETTINGS_TYPE_PLUGINS);
+
+ ClickAdd(controller);
+ EnterText(@"addedhost\n");
+
+ EXPECT_FALSE([controller editingNewException]);
+ [controller close];
+
+ HostContentSettingsMap::SettingsForOneType settings;
+ settingsMap_->GetSettingsForOneType(CONTENT_SETTINGS_TYPE_PLUGINS,
+ "",
+ &settings);
+ EXPECT_EQ(1u, settings.size());
+ EXPECT_EQ(HostContentSettingsMap::Pattern("addedhost"), settings[0].first);
+}
+
+TEST_F(ContentExceptionsWindowControllerTest, AddEscDoesNotAdd) {
+ ContentExceptionsWindowController* controller =
+ GetController(CONTENT_SETTINGS_TYPE_PLUGINS);
+
+ ClickAdd(controller);
+ EnterText(@"addedhost"); // but do not press enter
+ HitEscape(controller);
+
+ HostContentSettingsMap::SettingsForOneType settings;
+ settingsMap_->GetSettingsForOneType(CONTENT_SETTINGS_TYPE_PLUGINS,
+ "",
+ &settings);
+ EXPECT_EQ(0u, settings.size());
+ EXPECT_FALSE([controller editingNewException]);
+
+ [controller close];
+}
+
+// Regression test for http://crbug.com/37208
+TEST_F(ContentExceptionsWindowControllerTest, AddEditAddAdd) {
+ ContentExceptionsWindowController* controller =
+ GetController(CONTENT_SETTINGS_TYPE_PLUGINS);
+
+ ClickAdd(controller);
+ EnterText(@"testtesttest"); // but do not press enter
+ ClickAdd(controller);
+ ClickAdd(controller);
+
+ EXPECT_TRUE([controller editingNewException]);
+ [controller close];
+
+ HostContentSettingsMap::SettingsForOneType settings;
+ settingsMap_->GetSettingsForOneType(CONTENT_SETTINGS_TYPE_PLUGINS,
+ "",
+ &settings);
+ EXPECT_EQ(0u, settings.size());
+}
+
+TEST_F(ContentExceptionsWindowControllerTest, AddExistingEditAdd) {
+ settingsMap_->SetContentSetting(HostContentSettingsMap::Pattern("myhost"),
+ CONTENT_SETTINGS_TYPE_PLUGINS,
+ "",
+ CONTENT_SETTING_BLOCK);
+
+ ContentExceptionsWindowController* controller =
+ GetController(CONTENT_SETTINGS_TYPE_PLUGINS);
+
+ ClickAdd(controller);
+ EnterText(@"myhost"); // but do not press enter
+ ClickAdd(controller);
+
+ EXPECT_TRUE([controller editingNewException]);
+ [controller close];
+
+
+ HostContentSettingsMap::SettingsForOneType settings;
+ settingsMap_->GetSettingsForOneType(CONTENT_SETTINGS_TYPE_PLUGINS,
+ "",
+ &settings);
+ EXPECT_EQ(1u, settings.size());
+}
+
+TEST_F(ContentExceptionsWindowControllerTest, AddExistingDoesNotOverwrite) {
+ settingsMap_->SetContentSetting(HostContentSettingsMap::Pattern("myhost"),
+ CONTENT_SETTINGS_TYPE_COOKIES,
+ "",
+ CONTENT_SETTING_SESSION_ONLY);
+
+ ContentExceptionsWindowController* controller =
+ GetController(CONTENT_SETTINGS_TYPE_COOKIES);
+
+ ClickAdd(controller);
+ EnterText(@"myhost\n");
+
+ EXPECT_FALSE([controller editingNewException]);
+ [controller close];
+
+ HostContentSettingsMap::SettingsForOneType settings;
+ settingsMap_->GetSettingsForOneType(CONTENT_SETTINGS_TYPE_COOKIES,
+ "",
+ &settings);
+ EXPECT_EQ(1u, settings.size());
+ EXPECT_EQ(CONTENT_SETTING_SESSION_ONLY, settings[0].second);
+}
+
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/content_setting_bubble_cocoa.h b/chrome/browser/ui/cocoa/content_setting_bubble_cocoa.h
new file mode 100644
index 0000000..201ea12
--- /dev/null
+++ b/chrome/browser/ui/cocoa/content_setting_bubble_cocoa.h
@@ -0,0 +1,67 @@
+// 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 <map>
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#import "chrome/browser/ui/cocoa/base_bubble_controller.h"
+
+class ContentSettingBubbleModel;
+@class InfoBubbleView;
+
+namespace content_setting_bubble {
+// For every "show popup" button, remember the index of the popup tab contents
+// it should open when clicked.
+typedef std::map<NSButton*, int> PopupLinks;
+}
+
+// Manages a "content blocked" bubble.
+@interface ContentSettingBubbleController : BaseBubbleController {
+ @private
+ IBOutlet NSTextField* titleLabel_;
+ IBOutlet NSMatrix* allowBlockRadioGroup_;
+
+ IBOutlet NSButton* manageButton_;
+ IBOutlet NSButton* doneButton_;
+ IBOutlet NSButton* loadAllPluginsButton_;
+
+ // The container for the bubble contents of the geolocation bubble.
+ IBOutlet NSView* contentsContainer_;
+
+ // The info button of the cookies bubble.
+ IBOutlet NSButton* infoButton_;
+
+ IBOutlet NSTextField* blockedResourcesField_;
+
+ scoped_ptr<ContentSettingBubbleModel> contentSettingBubbleModel_;
+ content_setting_bubble::PopupLinks popupLinks_;
+}
+
+// Creates and shows a content blocked bubble. Takes ownership of
+// |contentSettingBubbleModel| but not of the other objects.
++ (ContentSettingBubbleController*)
+ showForModel:(ContentSettingBubbleModel*)contentSettingBubbleModel
+ parentWindow:(NSWindow*)parentWindow
+ anchoredAt:(NSPoint)anchoredAt;
+
+// Callback for the "don't block / continue blocking" radio group.
+- (IBAction)allowBlockToggled:(id)sender;
+
+// Callback for "close" button.
+- (IBAction)closeBubble:(id)sender;
+
+// Callback for "manage" button.
+- (IBAction)manageBlocking:(id)sender;
+
+// Callback for "info" link.
+- (IBAction)showMoreInfo:(id)sender;
+
+// Callback for "load all plugins" button.
+- (IBAction)loadAllPlugins:(id)sender;
+
+@end
diff --git a/chrome/browser/ui/cocoa/content_setting_bubble_cocoa.mm b/chrome/browser/ui/cocoa/content_setting_bubble_cocoa.mm
new file mode 100644
index 0000000..9e36292
--- /dev/null
+++ b/chrome/browser/ui/cocoa/content_setting_bubble_cocoa.mm
@@ -0,0 +1,487 @@
+// 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/ui/cocoa/content_setting_bubble_cocoa.h"
+
+#include "app/l10n_util.h"
+#include "base/command_line.h"
+#include "base/logging.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/blocked_content_container.h"
+#include "chrome/browser/content_setting_bubble_model.h"
+#include "chrome/browser/content_settings/host_content_settings_map.h"
+#include "chrome/browser/plugin_updater.h"
+#import "chrome/browser/ui/cocoa/content_settings_dialog_controller.h"
+#import "chrome/browser/ui/cocoa/hyperlink_button_cell.h"
+#import "chrome/browser/ui/cocoa/info_bubble_view.h"
+#import "chrome/browser/ui/cocoa/l10n_util.h"
+#include "chrome/common/chrome_switches.h"
+#include "grit/generated_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+#include "webkit/glue/plugins/plugin_list.h"
+
+namespace {
+
+// Must match the tag of the unblock radio button in the xib files.
+const int kAllowTag = 1;
+
+// Must match the tag of the block radio button in the xib files.
+const int kBlockTag = 2;
+
+// Height of one link in the popup list.
+const int kLinkHeight = 16;
+
+// Space between two popup links.
+const int kLinkPadding = 4;
+
+// Space taken in total by one popup link.
+const int kLinkLineHeight = kLinkHeight + kLinkPadding;
+
+// Space between popup list and surrounding UI elements.
+const int kLinkOuterPadding = 8;
+
+// Height of each of the labels in the geolocation bubble.
+const int kGeoLabelHeight = 14;
+
+// Height of the "Clear" button in the geolocation bubble.
+const int kGeoClearButtonHeight = 17;
+
+// Padding between radio buttons and "Load all plugins" button
+// in the plugin bubble.
+const int kLoadAllPluginsButtonVerticalPadding = 8;
+
+// General padding between elements in the geolocation bubble.
+const int kGeoPadding = 8;
+
+// Padding between host names in the geolocation bubble.
+const int kGeoHostPadding = 4;
+
+// Minimal padding between "Manage" and "Done" buttons.
+const int kManageDonePadding = 8;
+
+void SetControlSize(NSControl* control, NSControlSize controlSize) {
+ CGFloat fontSize = [NSFont systemFontSizeForControlSize:controlSize];
+ NSCell* cell = [control cell];
+ NSFont* font = [NSFont fontWithName:[[cell font] fontName] size:fontSize];
+ [cell setFont:font];
+ [cell setControlSize:controlSize];
+}
+
+// Returns an autoreleased NSTextField that is configured to look like a Label
+// looks in Interface Builder.
+NSTextField* LabelWithFrame(NSString* text, const NSRect& frame) {
+ NSTextField* label = [[NSTextField alloc] initWithFrame:frame];
+ [label setStringValue:text];
+ [label setSelectable:NO];
+ [label setBezeled:NO];
+ return [label autorelease];
+}
+
+} // namespace
+
+@interface ContentSettingBubbleController(Private)
+- (id)initWithModel:(ContentSettingBubbleModel*)settingsBubbleModel
+ parentWindow:(NSWindow*)parentWindow
+ anchoredAt:(NSPoint)anchoredAt;
+- (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame
+ title:(NSString*)title
+ icon:(NSImage*)icon
+ referenceFrame:(NSRect)referenceFrame;
+- (void)initializeBlockedPluginsList;
+- (void)initializeTitle;
+- (void)initializeRadioGroup;
+- (void)initializePopupList;
+- (void)initializeGeoLists;
+- (void)sizeToFitLoadPluginsButton;
+- (void)sizeToFitManageDoneButtons;
+- (void)removeInfoButton;
+- (void)popupLinkClicked:(id)sender;
+- (void)clearGeolocationForCurrentHost:(id)sender;
+@end
+
+@implementation ContentSettingBubbleController
+
++ (ContentSettingBubbleController*)
+ showForModel:(ContentSettingBubbleModel*)contentSettingBubbleModel
+ parentWindow:(NSWindow*)parentWindow
+ anchoredAt:(NSPoint)anchor {
+ // Autoreleases itself on bubble close.
+ return [[ContentSettingBubbleController alloc]
+ initWithModel:contentSettingBubbleModel
+ parentWindow:parentWindow
+ anchoredAt:anchor];
+}
+
+- (id)initWithModel:(ContentSettingBubbleModel*)contentSettingBubbleModel
+ parentWindow:(NSWindow*)parentWindow
+ anchoredAt:(NSPoint)anchoredAt {
+ // This method takes ownership of |contentSettingBubbleModel| in all cases.
+ scoped_ptr<ContentSettingBubbleModel> model(contentSettingBubbleModel);
+ DCHECK(model.get());
+
+ NSString* const nibPaths[] = {
+ @"ContentBlockedCookies",
+ @"ContentBlockedImages",
+ @"ContentBlockedJavaScript",
+ @"ContentBlockedPlugins",
+ @"ContentBlockedPopups",
+ @"ContentBubbleGeolocation",
+ @"", // Notifications do not have a bubble.
+ };
+ COMPILE_ASSERT(arraysize(nibPaths) == CONTENT_SETTINGS_NUM_TYPES,
+ nibPaths_requires_an_entry_for_every_setting_type);
+ const int settingsType = model->content_type();
+ // Nofifications do not have a bubble.
+ CHECK_NE(settingsType, CONTENT_SETTINGS_TYPE_NOTIFICATIONS);
+ DCHECK_LT(settingsType, CONTENT_SETTINGS_NUM_TYPES);
+ if ((self = [super initWithWindowNibPath:nibPaths[settingsType]
+ parentWindow:parentWindow
+ anchoredAt:anchoredAt])) {
+ contentSettingBubbleModel_.reset(model.release());
+ [self showWindow:nil];
+ }
+ return self;
+}
+
+- (void)initializeTitle {
+ if (!titleLabel_)
+ return;
+
+ NSString* label = base::SysUTF8ToNSString(
+ contentSettingBubbleModel_->bubble_content().title);
+ [titleLabel_ setStringValue:label];
+
+ // Layout title post-localization.
+ CGFloat deltaY = [GTMUILocalizerAndLayoutTweaker
+ sizeToFitFixedWidthTextField:titleLabel_];
+ NSRect windowFrame = [[self window] frame];
+ windowFrame.size.height += deltaY;
+ [[self window] setFrame:windowFrame display:NO];
+ NSRect titleFrame = [titleLabel_ frame];
+ titleFrame.origin.y -= deltaY;
+ [titleLabel_ setFrame:titleFrame];
+}
+
+- (void)initializeRadioGroup {
+ // Configure the radio group. For now, only deal with the
+ // strictly needed case of group containing 2 radio buttons.
+ const ContentSettingBubbleModel::RadioGroup& radio_group =
+ contentSettingBubbleModel_->bubble_content().radio_group;
+
+ // Select appropriate radio button.
+ [allowBlockRadioGroup_ selectCellWithTag:
+ radio_group.default_item == 0 ? kAllowTag : kBlockTag];
+
+ const ContentSettingBubbleModel::RadioItems& radio_items =
+ radio_group.radio_items;
+ DCHECK_EQ(2u, radio_items.size()) << "Only 2 radio items per group supported";
+ // Set radio group labels from model.
+ NSCell* radioCell = [allowBlockRadioGroup_ cellWithTag:kAllowTag];
+ [radioCell setTitle:base::SysUTF8ToNSString(radio_items[0])];
+
+ radioCell = [allowBlockRadioGroup_ cellWithTag:kBlockTag];
+ [radioCell setTitle:base::SysUTF8ToNSString(radio_items[1])];
+
+ // Layout radio group labels post-localization.
+ [GTMUILocalizerAndLayoutTweaker
+ wrapRadioGroupForWidth:allowBlockRadioGroup_];
+ CGFloat radioDeltaY = [GTMUILocalizerAndLayoutTweaker
+ sizeToFitView:allowBlockRadioGroup_].height;
+ NSRect windowFrame = [[self window] frame];
+ windowFrame.size.height += radioDeltaY;
+ [[self window] setFrame:windowFrame display:NO];
+}
+
+- (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame
+ title:(NSString*)title
+ icon:(NSImage*)icon
+ referenceFrame:(NSRect)referenceFrame {
+ scoped_nsobject<HyperlinkButtonCell> cell([[HyperlinkButtonCell alloc]
+ initTextCell:title]);
+ [cell.get() setAlignment:NSNaturalTextAlignment];
+ if (icon) {
+ [cell.get() setImagePosition:NSImageLeft];
+ [cell.get() setImage:icon];
+ } else {
+ [cell.get() setImagePosition:NSNoImage];
+ }
+ [cell.get() setControlSize:NSSmallControlSize];
+
+ NSButton* button = [[[NSButton alloc] initWithFrame:frame] autorelease];
+ // Cell must be set immediately after construction.
+ [button setCell:cell.get()];
+
+ // If the link text is too long, clamp it.
+ [button sizeToFit];
+ int maxWidth = NSWidth([[self bubble] frame]) - 2 * NSMinX(referenceFrame);
+ NSRect buttonFrame = [button frame];
+ if (NSWidth(buttonFrame) > maxWidth) {
+ buttonFrame.size.width = maxWidth;
+ [button setFrame:buttonFrame];
+ }
+
+ [button setTarget:self];
+ [button setAction:@selector(popupLinkClicked:)];
+ return button;
+}
+
+- (void)initializeBlockedPluginsList {
+ NSMutableArray* pluginArray = [NSMutableArray array];
+ const std::set<std::string>& plugins =
+ contentSettingBubbleModel_->bubble_content().resource_identifiers;
+ if (plugins.empty()) {
+ int delta = NSMinY([titleLabel_ frame]) -
+ NSMinY([blockedResourcesField_ frame]);
+ [blockedResourcesField_ removeFromSuperview];
+ NSRect frame = [[self window] frame];
+ frame.size.height -= delta;
+ [[self window] setFrame:frame display:NO];
+ } else {
+ for (std::set<std::string>::iterator it = plugins.begin();
+ it != plugins.end(); ++it) {
+ NSString* name;
+ NPAPI::PluginList::PluginMap groups;
+ NPAPI::PluginList::Singleton()->GetPluginGroups(false, &groups);
+ if (groups.find(*it) != groups.end())
+ name = base::SysUTF16ToNSString(groups[*it]->GetGroupName());
+ else
+ name = base::SysUTF8ToNSString(*it);
+ [pluginArray addObject:name];
+ }
+ [blockedResourcesField_
+ setStringValue:[pluginArray componentsJoinedByString:@"\n"]];
+ [GTMUILocalizerAndLayoutTweaker
+ sizeToFitFixedWidthTextField:blockedResourcesField_];
+ }
+}
+
+- (void)initializePopupList {
+ // I didn't put the buttons into a NSMatrix because then they are only one
+ // entity in the key view loop. This way, one can tab through all of them.
+ const ContentSettingBubbleModel::PopupItems& popupItems =
+ contentSettingBubbleModel_->bubble_content().popup_items;
+
+ // Get the pre-resize frame of the radio group. Its origin is where the
+ // popup list should go.
+ NSRect radioFrame = [allowBlockRadioGroup_ frame];
+
+ // Make room for the popup list. The bubble view and its subviews autosize
+ // themselves when the window is enlarged.
+ // Heading and radio box are already 1 * kLinkOuterPadding apart in the nib,
+ // so only 1 * kLinkOuterPadding more is needed.
+ int delta = popupItems.size() * kLinkLineHeight - kLinkPadding +
+ kLinkOuterPadding;
+ NSSize deltaSize = NSMakeSize(0, delta);
+ deltaSize = [[[self window] contentView] convertSize:deltaSize toView:nil];
+ NSRect windowFrame = [[self window] frame];
+ windowFrame.size.height += deltaSize.height;
+ [[self window] setFrame:windowFrame display:NO];
+
+ // Create popup list.
+ int topLinkY = NSMaxY(radioFrame) + delta - kLinkHeight;
+ int row = 0;
+ for (std::vector<ContentSettingBubbleModel::PopupItem>::const_iterator
+ it(popupItems.begin()); it != popupItems.end(); ++it, ++row) {
+ const SkBitmap& icon = it->bitmap;
+ NSImage* image = nil;
+ if (!icon.empty())
+ image = gfx::SkBitmapToNSImage(icon);
+
+ std::string title(it->title);
+ // The popup may not have committed a load yet, in which case it won't
+ // have a URL or title.
+ if (title.empty())
+ title = l10n_util::GetStringUTF8(IDS_TAB_LOADING_TITLE);
+
+ NSRect linkFrame =
+ NSMakeRect(NSMinX(radioFrame), topLinkY - kLinkLineHeight * row,
+ 200, kLinkHeight);
+ NSButton* button = [self
+ hyperlinkButtonWithFrame:linkFrame
+ title:base::SysUTF8ToNSString(title)
+ icon:image
+ referenceFrame:radioFrame];
+ [[self bubble] addSubview:button];
+ popupLinks_[button] = row;
+ }
+}
+
+- (void)initializeGeoLists {
+ // Cocoa has its origin in the lower left corner. This means elements are
+ // added from bottom to top, which explains why loops run backwards and the
+ // order of operations is the other way than on Linux/Windows.
+ const ContentSettingBubbleModel::BubbleContent& content =
+ contentSettingBubbleModel_->bubble_content();
+ NSRect containerFrame = [contentsContainer_ frame];
+ NSRect frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight);
+
+ // "Clear" button.
+ if (!content.clear_link.empty()) {
+ NSRect buttonFrame = NSMakeRect(0, 0,
+ NSWidth(containerFrame),
+ kGeoClearButtonHeight);
+ scoped_nsobject<NSButton> button([[NSButton alloc]
+ initWithFrame:buttonFrame]);
+ [button setTitle:base::SysUTF8ToNSString(content.clear_link)];
+ [button setTarget:self];
+ [button setAction:@selector(clearGeolocationForCurrentHost:)];
+ [button setBezelStyle:NSRoundRectBezelStyle];
+ SetControlSize(button, NSSmallControlSize);
+ [button sizeToFit];
+
+ // If the button is wider than the container, widen the window.
+ CGFloat buttonWidth = NSWidth([button frame]);
+ if (buttonWidth > NSWidth(containerFrame)) {
+ NSRect windowFrame = [[self window] frame];
+ windowFrame.size.width += buttonWidth - NSWidth(containerFrame);
+ [[self window] setFrame:windowFrame display:NO];
+ // Fetch the updated sizes.
+ containerFrame = [contentsContainer_ frame];
+ frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight);
+ }
+
+ // Add the button.
+ [contentsContainer_ addSubview:button];
+
+ frame.origin.y = NSMaxY([button frame]) + kGeoPadding;
+ }
+
+ typedef
+ std::vector<ContentSettingBubbleModel::DomainList>::const_reverse_iterator
+ GeolocationGroupIterator;
+ for (GeolocationGroupIterator i = content.domain_lists.rbegin();
+ i != content.domain_lists.rend(); ++i) {
+ // Add all hosts in the current domain list.
+ for (std::set<std::string>::const_reverse_iterator j = i->hosts.rbegin();
+ j != i->hosts.rend(); ++j) {
+ NSTextField* title = LabelWithFrame(base::SysUTF8ToNSString(*j), frame);
+ SetControlSize(title, NSSmallControlSize);
+ [contentsContainer_ addSubview:title];
+
+ frame.origin.y = NSMaxY(frame) + kGeoHostPadding +
+ [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
+ }
+ if (!i->hosts.empty())
+ frame.origin.y += kGeoPadding - kGeoHostPadding;
+
+ // Add the domain list's title.
+ NSTextField* title =
+ LabelWithFrame(base::SysUTF8ToNSString(i->title), frame);
+ SetControlSize(title, NSSmallControlSize);
+ [contentsContainer_ addSubview:title];
+
+ frame.origin.y = NSMaxY(frame) + kGeoPadding +
+ [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
+ }
+
+ CGFloat containerHeight = frame.origin.y;
+ // Undo last padding.
+ if (!content.domain_lists.empty())
+ containerHeight -= kGeoPadding;
+
+ // Resize container to fit its subviews, and window to fit the container.
+ NSRect windowFrame = [[self window] frame];
+ windowFrame.size.height += containerHeight - NSHeight(containerFrame);
+ [[self window] setFrame:windowFrame display:NO];
+ containerFrame.size.height = containerHeight;
+ [contentsContainer_ setFrame:containerFrame];
+}
+
+- (void)sizeToFitLoadPluginsButton {
+ const ContentSettingBubbleModel::BubbleContent& content =
+ contentSettingBubbleModel_->bubble_content();
+ [loadAllPluginsButton_ setEnabled:content.load_plugins_link_enabled];
+
+ // Resize horizontally to fit button if necessary.
+ NSRect windowFrame = [[self window] frame];
+ int widthNeeded = NSWidth([loadAllPluginsButton_ frame]) +
+ 2 * NSMinX([loadAllPluginsButton_ frame]);
+ if (NSWidth(windowFrame) < widthNeeded) {
+ windowFrame.size.width = widthNeeded;
+ [[self window] setFrame:windowFrame display:NO];
+ }
+}
+
+- (void)sizeToFitManageDoneButtons {
+ CGFloat actualWidth = NSWidth([[[self window] contentView] frame]);
+ CGFloat requiredWidth = NSMaxX([manageButton_ frame]) + kManageDonePadding +
+ NSWidth([[doneButton_ superview] frame]) - NSMinX([doneButton_ frame]);
+ if (requiredWidth <= actualWidth || !doneButton_ || !manageButton_)
+ return;
+
+ // Resize window, autoresizing takes care of the rest.
+ NSSize size = NSMakeSize(requiredWidth - actualWidth, 0);
+ size = [[[self window] contentView] convertSize:size toView:nil];
+ NSRect frame = [[self window] frame];
+ frame.origin.x -= size.width;
+ frame.size.width += size.width;
+ [[self window] setFrame:frame display:NO];
+}
+
+- (void)awakeFromNib {
+ [super awakeFromNib];
+
+ [[self bubble] setBubbleType:info_bubble::kWhiteInfoBubble];
+ [[self bubble] setArrowLocation:info_bubble::kTopRight];
+
+ // Adapt window size to bottom buttons. Do this before all other layouting.
+ [self sizeToFitManageDoneButtons];
+
+ [self initializeTitle];
+
+ ContentSettingsType type = contentSettingBubbleModel_->content_type();
+ if (type == CONTENT_SETTINGS_TYPE_PLUGINS) {
+ [self sizeToFitLoadPluginsButton];
+ [self initializeBlockedPluginsList];
+ }
+ if (allowBlockRadioGroup_) // not bound in cookie bubble xib
+ [self initializeRadioGroup];
+
+ if (type == CONTENT_SETTINGS_TYPE_POPUPS)
+ [self initializePopupList];
+ if (type == CONTENT_SETTINGS_TYPE_GEOLOCATION)
+ [self initializeGeoLists];
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Actual application logic
+
+- (IBAction)allowBlockToggled:(id)sender {
+ NSButtonCell *selectedCell = [sender selectedCell];
+ contentSettingBubbleModel_->OnRadioClicked(
+ [selectedCell tag] == kAllowTag ? 0 : 1);
+}
+
+- (IBAction)closeBubble:(id)sender {
+ [self close];
+}
+
+- (IBAction)manageBlocking:(id)sender {
+ contentSettingBubbleModel_->OnManageLinkClicked();
+}
+
+- (IBAction)showMoreInfo:(id)sender {
+ contentSettingBubbleModel_->OnInfoLinkClicked();
+ [self close];
+}
+
+- (IBAction)loadAllPlugins:(id)sender {
+ contentSettingBubbleModel_->OnLoadPluginsLinkClicked();
+ [self close];
+}
+
+- (void)popupLinkClicked:(id)sender {
+ content_setting_bubble::PopupLinks::iterator i(popupLinks_.find(sender));
+ DCHECK(i != popupLinks_.end());
+ contentSettingBubbleModel_->OnPopupClicked(i->second);
+}
+
+- (void)clearGeolocationForCurrentHost:(id)sender {
+ contentSettingBubbleModel_->OnClearLinkClicked();
+ [self close];
+}
+
+@end // ContentSettingBubbleController
diff --git a/chrome/browser/ui/cocoa/content_setting_bubble_cocoa_unittest.mm b/chrome/browser/ui/cocoa/content_setting_bubble_cocoa_unittest.mm
new file mode 100644
index 0000000..e67b0aa
--- /dev/null
+++ b/chrome/browser/ui/cocoa/content_setting_bubble_cocoa_unittest.mm
@@ -0,0 +1,63 @@
+// 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/ui/cocoa/content_setting_bubble_cocoa.h"
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/debug/debugger.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/content_setting_bubble_model.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/common/content_settings_types.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+class DummyContentSettingBubbleModel : public ContentSettingBubbleModel {
+ public:
+ DummyContentSettingBubbleModel(ContentSettingsType content_type)
+ : ContentSettingBubbleModel(NULL, NULL, content_type) {
+ RadioGroup radio_group;
+ radio_group.default_item = 0;
+ radio_group.radio_items.resize(2);
+ set_radio_group(radio_group);
+ }
+};
+
+class ContentSettingBubbleControllerTest : public CocoaTest {
+};
+
+// Check that the bubble doesn't crash or leak for any settings type
+TEST_F(ContentSettingBubbleControllerTest, Init) {
+ for (int i = 0; i < CONTENT_SETTINGS_NUM_TYPES; ++i) {
+ if (i == CONTENT_SETTINGS_TYPE_NOTIFICATIONS)
+ continue; // Notifications have no bubble.
+
+ ContentSettingsType settingsType = static_cast<ContentSettingsType>(i);
+
+ scoped_nsobject<NSWindow> parent([[NSWindow alloc]
+ initWithContentRect:NSMakeRect(0, 0, 800, 600)
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:NO]);
+ [parent setReleasedWhenClosed:NO];
+ if (base::debug::BeingDebugged())
+ [parent.get() orderFront:nil];
+ else
+ [parent.get() orderBack:nil];
+
+ ContentSettingBubbleController* controller = [ContentSettingBubbleController
+ showForModel:new DummyContentSettingBubbleModel(settingsType)
+ parentWindow:parent
+ anchoredAt:NSMakePoint(50, 20)];
+ EXPECT_TRUE(controller != nil);
+ EXPECT_TRUE([[controller window] isVisible]);
+ [parent.get() close];
+ }
+}
+
+} // namespace
+
+
diff --git a/chrome/browser/ui/cocoa/content_settings_dialog_controller.h b/chrome/browser/ui/cocoa/content_settings_dialog_controller.h
new file mode 100644
index 0000000..432a3dc
--- /dev/null
+++ b/chrome/browser/ui/cocoa/content_settings_dialog_controller.h
@@ -0,0 +1,102 @@
+// 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 "base/cocoa_protocols_mac.h"
+#include "base/scoped_ptr.h"
+#include "chrome/common/content_settings_types.h"
+#include "chrome/browser/prefs/pref_change_registrar.h"
+#include "chrome/browser/prefs/pref_member.h"
+
+// Index of the "enabled" and "disabled" radio group settings in all tabs except
+// the ones below.
+const NSInteger kContentSettingsEnabledIndex = 0;
+const NSInteger kContentSettingsDisabledIndex = 1;
+
+// Indices of the various cookie settings in the cookie radio group.
+const NSInteger kCookieEnabledIndex = 0;
+const NSInteger kCookieDisabledIndex = 1;
+
+// Indices of the various plugin settings in the plugins radio group.
+const NSInteger kPluginsAllowIndex = 0;
+const NSInteger kPluginsAskIndex = 1;
+const NSInteger kPluginsBlockIndex = 2;
+
+// Indices of the various geolocation settings in the geolocation radio group.
+const NSInteger kGeolocationEnabledIndex = 0;
+const NSInteger kGeolocationAskIndex = 1;
+const NSInteger kGeolocationDisabledIndex = 2;
+
+// Indices of the various notifications settings in the geolocation radio group.
+const NSInteger kNotificationsEnabledIndex = 0;
+const NSInteger kNotificationsAskIndex = 1;
+const NSInteger kNotificationsDisabledIndex = 2;
+
+namespace ContentSettingsDialogControllerInternal {
+class PrefObserverBridge;
+}
+
+class Profile;
+@class TabViewPickerTable;
+
+// This controller manages a dialog that lets the user manage the content
+// settings for several content setting types.
+@interface ContentSettingsDialogController
+ : NSWindowController<NSWindowDelegate, NSTabViewDelegate> {
+ @private
+ IBOutlet NSTabView* tabView_;
+ IBOutlet TabViewPickerTable* tabViewPicker_;
+ IBOutlet NSMatrix* pluginDefaultSettingMatrix_;
+ Profile* profile_; // weak
+ IntegerPrefMember lastSelectedTab_;
+ BooleanPrefMember clearSiteDataOnExit_;
+ PrefChangeRegistrar registrar_;
+ scoped_ptr<ContentSettingsDialogControllerInternal::PrefObserverBridge>
+ observer_; // Watches for pref changes.
+}
+
+// Show the content settings dialog associated with the given profile (or the
+// original profile if this is an incognito profile). If no content settings
+// dialog exists for this profile, create one and show it. Any resulting
+// editor releases itself when closed.
++(id)showContentSettingsForType:(ContentSettingsType)settingsType
+ profile:(Profile*)profile;
+
+// Closes an exceptions sheet, if one is attached.
+- (void)closeExceptionsSheet;
+
+- (IBAction)showCookies:(id)sender;
+- (IBAction)openFlashPlayerSettings:(id)sender;
+- (IBAction)openPluginsPage:(id)sender;
+
+- (IBAction)showCookieExceptions:(id)sender;
+- (IBAction)showImagesExceptions:(id)sender;
+- (IBAction)showJavaScriptExceptions:(id)sender;
+- (IBAction)showPluginsExceptions:(id)sender;
+- (IBAction)showPopupsExceptions:(id)sender;
+- (IBAction)showGeolocationExceptions:(id)sender;
+- (IBAction)showNotificationsExceptions:(id)sender;
+
+@end
+
+@interface ContentSettingsDialogController (TestingAPI)
+// Properties that the radio groups and checkboxes are bound to.
+@property(nonatomic) NSInteger cookieSettingIndex;
+@property(nonatomic) BOOL blockThirdPartyCookies;
+@property(nonatomic) BOOL clearSiteDataOnExit;
+@property(nonatomic) NSInteger imagesEnabledIndex;
+@property(nonatomic) NSInteger javaScriptEnabledIndex;
+@property(nonatomic) NSInteger popupsEnabledIndex;
+@property(nonatomic) NSInteger pluginsEnabledIndex;
+@property(nonatomic) NSInteger geolocationSettingIndex;
+@property(nonatomic) NSInteger notificationsSettingIndex;
+
+@property(nonatomic, readonly) BOOL blockThirdPartyCookiesManaged;
+@property(nonatomic, readonly) BOOL cookieSettingsManaged;
+@property(nonatomic, readonly) BOOL imagesSettingsManaged;
+@property(nonatomic, readonly) BOOL javaScriptSettingsManaged;
+@property(nonatomic, readonly) BOOL pluginsSettingsManaged;
+@property(nonatomic, readonly) BOOL popupsSettingsManaged;
+@end
diff --git a/chrome/browser/ui/cocoa/content_settings_dialog_controller.mm b/chrome/browser/ui/cocoa/content_settings_dialog_controller.mm
new file mode 100644
index 0000000..7a574d0
--- /dev/null
+++ b/chrome/browser/ui/cocoa/content_settings_dialog_controller.mm
@@ -0,0 +1,647 @@
+// 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/ui/cocoa/content_settings_dialog_controller.h"
+
+#import <Cocoa/Cocoa.h>
+
+#include "app/l10n_util.h"
+#include "base/command_line.h"
+#include "base/mac_util.h"
+#import "chrome/browser/content_settings/host_content_settings_map.h"
+#import "chrome/browser/geolocation/geolocation_content_settings_map.h"
+#import "chrome/browser/geolocation/geolocation_exceptions_table_model.h"
+#import "chrome/browser/notifications/desktop_notification_service.h"
+#import "chrome/browser/notifications/notification_exceptions_table_model.h"
+#include "chrome/browser/plugin_exceptions_table_model.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_window.h"
+#import "chrome/browser/ui/cocoa/content_exceptions_window_controller.h"
+#import "chrome/browser/ui/cocoa/cookies_window_controller.h"
+#import "chrome/browser/ui/cocoa/l10n_util.h"
+#import "chrome/browser/ui/cocoa/simple_content_exceptions_window_controller.h"
+#import "chrome/browser/ui/cocoa/tab_view_picker_table.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/url_constants.h"
+#include "grit/locale_settings.h"
+#include "grit/generated_resources.h"
+
+namespace {
+
+// Stores the currently visible content settings dialog, if any.
+ContentSettingsDialogController* g_instance = nil;
+
+} // namespace
+
+
+@interface ContentSettingsDialogController(Private)
+- (id)initWithProfile:(Profile*)profile;
+- (void)selectTab:(ContentSettingsType)settingsType;
+- (void)showExceptionsForType:(ContentSettingsType)settingsType;
+
+// Callback when preferences are changed. |prefName| is the name of the
+// pref that has changed.
+- (void)prefChanged:(const std::string&)prefName;
+
+// Callback when content settings are changed.
+- (void)contentSettingsChanged:
+ (HostContentSettingsMap::ContentSettingsDetails*)details;
+
+@end
+
+namespace ContentSettingsDialogControllerInternal {
+
+// A C++ class registered for changes in preferences.
+class PrefObserverBridge : public NotificationObserver {
+ public:
+ PrefObserverBridge(ContentSettingsDialogController* controller)
+ : controller_(controller), disabled_(false) {}
+
+ virtual ~PrefObserverBridge() {}
+
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ if (disabled_)
+ return;
+
+ // This is currently used by most notifications.
+ if (type == NotificationType::PREF_CHANGED) {
+ std::string* detail = Details<std::string>(details).ptr();
+ if (detail)
+ [controller_ prefChanged:*detail];
+ }
+
+ // This is sent when the "is managed" state changes.
+ // TODO(markusheintz): Move all content settings to this notification.
+ if (type == NotificationType::CONTENT_SETTINGS_CHANGED) {
+ HostContentSettingsMap::ContentSettingsDetails* settings_details =
+ Details<HostContentSettingsMap::ContentSettingsDetails>(details).ptr();
+ [controller_ contentSettingsChanged:settings_details];
+ }
+ }
+
+ void SetDisabled(bool disabled) {
+ disabled_ = disabled;
+ }
+
+ private:
+ ContentSettingsDialogController* controller_; // weak, owns us
+ bool disabled_; // true if notifications should be ignored.
+};
+
+// A C++ utility class to disable notifications for PrefsObserverBridge.
+// The intended usage is to create this on the stack.
+class PrefObserverDisabler {
+ public:
+ PrefObserverDisabler(PrefObserverBridge *bridge) : bridge_(bridge) {
+ bridge_->SetDisabled(true);
+ }
+
+ ~PrefObserverDisabler() {
+ bridge_->SetDisabled(false);
+ }
+
+ private:
+ PrefObserverBridge *bridge_;
+};
+
+} // ContentSettingsDialogControllerInternal
+
+@implementation ContentSettingsDialogController
+
++ (id)showContentSettingsForType:(ContentSettingsType)settingsType
+ profile:(Profile*)profile {
+ profile = profile->GetOriginalProfile();
+ if (!g_instance)
+ g_instance = [[self alloc] initWithProfile:profile];
+
+ // The code doesn't expect multiple profiles. Check that support for that
+ // hasn't been added.
+ DCHECK(g_instance->profile_ == profile);
+
+ // Select desired tab.
+ if (settingsType == CONTENT_SETTINGS_TYPE_DEFAULT) {
+ // Remember the last visited page from local state.
+ int value = g_instance->lastSelectedTab_.GetValue();
+ if (value >= 0 && value < CONTENT_SETTINGS_NUM_TYPES)
+ settingsType = static_cast<ContentSettingsType>(value);
+ if (settingsType == CONTENT_SETTINGS_TYPE_DEFAULT)
+ settingsType = CONTENT_SETTINGS_TYPE_COOKIES;
+ }
+ // TODO(thakis): Autosave window pos.
+
+ [g_instance selectTab:settingsType];
+ [g_instance showWindow:nil];
+ [g_instance closeExceptionsSheet];
+ return g_instance;
+}
+
+- (id)initWithProfile:(Profile*)profile {
+ DCHECK(profile);
+ NSString* nibpath =
+ [mac_util::MainAppBundle() pathForResource:@"ContentSettings"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ profile_ = profile;
+
+ observer_.reset(
+ new ContentSettingsDialogControllerInternal::PrefObserverBridge(self));
+ clearSiteDataOnExit_.Init(prefs::kClearSiteDataOnExit,
+ profile_->GetPrefs(), observer_.get());
+
+ // Manually observe notifications for preferences that are grouped in
+ // the HostContentSettingsMap or GeolocationContentSettingsMap.
+ PrefService* prefs = profile_->GetPrefs();
+ registrar_.Init(prefs);
+ registrar_.Add(prefs::kBlockThirdPartyCookies, observer_.get());
+ registrar_.Add(prefs::kBlockNonsandboxedPlugins, observer_.get());
+ registrar_.Add(prefs::kDefaultContentSettings, observer_.get());
+ registrar_.Add(prefs::kGeolocationDefaultContentSetting, observer_.get());
+
+ // We don't need to observe changes in this value.
+ lastSelectedTab_.Init(prefs::kContentSettingsWindowLastTabIndex,
+ profile_->GetPrefs(), NULL);
+ }
+ return self;
+}
+
+- (void)closeExceptionsSheet {
+ NSWindow* attachedSheet = [[self window] attachedSheet];
+ if (attachedSheet) {
+ [NSApp endSheet:attachedSheet];
+ }
+}
+
+- (void)awakeFromNib {
+ DCHECK([self window]);
+ DCHECK(tabView_);
+ DCHECK(tabViewPicker_);
+ DCHECK_EQ(self, [[self window] delegate]);
+
+ // Adapt views to potentially long localized strings.
+ CGFloat windowDelta = 0;
+ for (NSTabViewItem* tab in [tabView_ tabViewItems]) {
+ NSArray* subviews = [[tab view] subviews];
+ windowDelta = MAX(windowDelta,
+ cocoa_l10n_util::VerticallyReflowGroup(subviews));
+
+ for (NSView* view in subviews) {
+ // Since the tab pane is in a horizontal resizer in IB, it's convenient
+ // to give all the subviews flexible width so that their sizes are
+ // autoupdated in IB. However, in chrome, the subviews shouldn't have
+ // flexible widths as this looks weird.
+ [view setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
+ }
+ }
+
+ NSString* label =
+ l10n_util::GetNSStringWithFixup(IDS_CONTENT_SETTINGS_FEATURES_LABEL);
+ label = [label stringByReplacingOccurrencesOfString:@":" withString:@""];
+ [tabViewPicker_ setHeading:label];
+
+ if (!CommandLine::ForCurrentProcess()->HasSwitch(
+ switches::kEnableClickToPlay)) {
+ // The |pluginsEnabledIndex| property is bound to the selected *tag*,
+ // so we don't have to worry about index shifts when removing a row
+ // from the matrix.
+ [pluginDefaultSettingMatrix_ removeRow:kPluginsAskIndex];
+ NSArray* siblingViews = [[pluginDefaultSettingMatrix_ superview] subviews];
+ for (NSView* view in siblingViews) {
+ NSRect frame = [view frame];
+ if (frame.origin.y < [pluginDefaultSettingMatrix_ frame].origin.y) {
+ frame.origin.y +=
+ ([pluginDefaultSettingMatrix_ cellSize].height +
+ [pluginDefaultSettingMatrix_ intercellSpacing].height);
+ [view setFrame:frame];
+ }
+ }
+ }
+
+ NSRect frame = [[self window] frame];
+ frame.origin.y -= windowDelta;
+ frame.size.height += windowDelta;
+ [[self window] setFrame:frame display:NO];
+}
+
+// NSWindowDelegate method.
+- (void)windowWillClose:(NSNotification*)notification {
+ [self autorelease];
+ g_instance = nil;
+}
+
+- (void)selectTab:(ContentSettingsType)settingsType {
+ [self window]; // Make sure the nib file is loaded.
+ DCHECK(tabView_);
+ [tabView_ selectTabViewItemAtIndex:settingsType];
+}
+
+// NSTabViewDelegate method.
+- (void) tabView:(NSTabView*)tabView
+ didSelectTabViewItem:(NSTabViewItem*)tabViewItem {
+ DCHECK_EQ(tabView_, tabView);
+ NSInteger index = [tabView indexOfTabViewItem:tabViewItem];
+ DCHECK_GT(index, CONTENT_SETTINGS_TYPE_DEFAULT);
+ DCHECK_LT(index, CONTENT_SETTINGS_NUM_TYPES);
+ if (index > CONTENT_SETTINGS_TYPE_DEFAULT &&
+ index < CONTENT_SETTINGS_NUM_TYPES)
+ lastSelectedTab_.SetValue(index);
+}
+
+// Let esc close the window.
+- (void)cancel:(id)sender {
+ [self close];
+}
+
+- (void)setCookieSettingIndex:(NSInteger)value {
+ ContentSetting setting = CONTENT_SETTING_DEFAULT;
+ switch (value) {
+ case kCookieEnabledIndex: setting = CONTENT_SETTING_ALLOW; break;
+ case kCookieDisabledIndex: setting = CONTENT_SETTING_BLOCK; break;
+ default:
+ NOTREACHED();
+ }
+ ContentSettingsDialogControllerInternal::PrefObserverDisabler
+ disabler(observer_.get());
+ profile_->GetHostContentSettingsMap()->SetDefaultContentSetting(
+ CONTENT_SETTINGS_TYPE_COOKIES,
+ setting);
+}
+
+- (NSInteger)cookieSettingIndex {
+ switch (profile_->GetHostContentSettingsMap()->GetDefaultContentSetting(
+ CONTENT_SETTINGS_TYPE_COOKIES)) {
+ case CONTENT_SETTING_ALLOW: return kCookieEnabledIndex;
+ case CONTENT_SETTING_BLOCK: return kCookieDisabledIndex;
+ default:
+ NOTREACHED();
+ return kCookieEnabledIndex;
+ }
+}
+
+- (BOOL)cookieSettingsManaged {
+ return profile_->GetHostContentSettingsMap()->IsDefaultContentSettingManaged(
+ CONTENT_SETTINGS_TYPE_COOKIES);
+}
+
+- (BOOL)blockThirdPartyCookies {
+ HostContentSettingsMap* settingsMap = profile_->GetHostContentSettingsMap();
+ return settingsMap->BlockThirdPartyCookies();
+}
+
+- (void)setBlockThirdPartyCookies:(BOOL)value {
+ HostContentSettingsMap* settingsMap = profile_->GetHostContentSettingsMap();
+ ContentSettingsDialogControllerInternal::PrefObserverDisabler
+ disabler(observer_.get());
+ settingsMap->SetBlockThirdPartyCookies(value);
+}
+
+- (BOOL)blockThirdPartyCookiesManaged {
+ HostContentSettingsMap* settingsMap = profile_->GetHostContentSettingsMap();
+ return settingsMap->IsBlockThirdPartyCookiesManaged();
+}
+
+- (BOOL)clearSiteDataOnExit {
+ return clearSiteDataOnExit_.GetValue();
+}
+
+- (void)setClearSiteDataOnExit:(BOOL)value {
+ ContentSettingsDialogControllerInternal::PrefObserverDisabler
+ disabler(observer_.get());
+ clearSiteDataOnExit_.SetValue(value);
+}
+
+// Shows the cookies controller.
+- (IBAction)showCookies:(id)sender {
+ // The cookie controller will autorelease itself when it's closed.
+ BrowsingDataDatabaseHelper* databaseHelper =
+ new BrowsingDataDatabaseHelper(profile_);
+ BrowsingDataLocalStorageHelper* storageHelper =
+ new BrowsingDataLocalStorageHelper(profile_);
+ BrowsingDataAppCacheHelper* appcacheHelper =
+ new BrowsingDataAppCacheHelper(profile_);
+ BrowsingDataIndexedDBHelper* indexedDBHelper =
+ BrowsingDataIndexedDBHelper::Create(profile_);
+ CookiesWindowController* controller =
+ [[CookiesWindowController alloc] initWithProfile:profile_
+ databaseHelper:databaseHelper
+ storageHelper:storageHelper
+ appcacheHelper:appcacheHelper
+ indexedDBHelper:indexedDBHelper];
+ [controller attachSheetTo:[self window]];
+}
+
+// Called when the user clicks the "Flash Player storage settings" button.
+- (IBAction)openFlashPlayerSettings:(id)sender {
+ Browser* browser = Browser::Create(profile_);
+ browser->OpenURL(GURL(l10n_util::GetStringUTF8(IDS_FLASH_STORAGE_URL)),
+ GURL(), NEW_FOREGROUND_TAB, PageTransition::LINK);
+ browser->window()->Show();
+}
+
+// Called when the user clicks the "Disable individual plug-ins..." button.
+- (IBAction)openPluginsPage:(id)sender {
+ Browser* browser = Browser::Create(profile_);
+ browser->OpenURL(GURL(chrome::kChromeUIPluginsURL),
+ GURL(), NEW_FOREGROUND_TAB, PageTransition::LINK);
+ browser->window()->Show();
+}
+
+- (IBAction)showCookieExceptions:(id)sender {
+ [self showExceptionsForType:CONTENT_SETTINGS_TYPE_COOKIES];
+}
+
+- (IBAction)showImagesExceptions:(id)sender {
+ [self showExceptionsForType:CONTENT_SETTINGS_TYPE_IMAGES];
+}
+
+- (IBAction)showJavaScriptExceptions:(id)sender {
+ [self showExceptionsForType:CONTENT_SETTINGS_TYPE_JAVASCRIPT];
+}
+
+- (IBAction)showPluginsExceptions:(id)sender {
+ if (CommandLine::ForCurrentProcess()->HasSwitch(
+ switches::kEnableResourceContentSettings)) {
+ HostContentSettingsMap* settingsMap = profile_->GetHostContentSettingsMap();
+ HostContentSettingsMap* offTheRecordSettingsMap =
+ profile_->HasOffTheRecordProfile() ?
+ profile_->GetOffTheRecordProfile()->GetHostContentSettingsMap() :
+ NULL;
+ PluginExceptionsTableModel* model =
+ new PluginExceptionsTableModel(settingsMap, offTheRecordSettingsMap);
+ model->LoadSettings();
+ [[SimpleContentExceptionsWindowController controllerWithTableModel:model]
+ attachSheetTo:[self window]];
+ } else {
+ [self showExceptionsForType:CONTENT_SETTINGS_TYPE_PLUGINS];
+ }
+}
+
+- (IBAction)showPopupsExceptions:(id)sender {
+ [self showExceptionsForType:CONTENT_SETTINGS_TYPE_POPUPS];
+}
+
+- (IBAction)showGeolocationExceptions:(id)sender {
+ GeolocationContentSettingsMap* settingsMap =
+ profile_->GetGeolocationContentSettingsMap();
+ GeolocationExceptionsTableModel* model = // Freed by window controller.
+ new GeolocationExceptionsTableModel(settingsMap);
+ [[SimpleContentExceptionsWindowController controllerWithTableModel:model]
+ attachSheetTo:[self window]];
+}
+
+- (IBAction)showNotificationsExceptions:(id)sender {
+ DesktopNotificationService* service =
+ profile_->GetDesktopNotificationService();
+ NotificationExceptionsTableModel* model = // Freed by window controller.
+ new NotificationExceptionsTableModel(service);
+ [[SimpleContentExceptionsWindowController controllerWithTableModel:model]
+ attachSheetTo:[self window]];
+}
+
+- (void)showExceptionsForType:(ContentSettingsType)settingsType {
+ HostContentSettingsMap* settingsMap = profile_->GetHostContentSettingsMap();
+ HostContentSettingsMap* offTheRecordSettingsMap =
+ profile_->HasOffTheRecordProfile() ?
+ profile_->GetOffTheRecordProfile()->GetHostContentSettingsMap() :
+ NULL;
+ [[ContentExceptionsWindowController controllerForType:settingsType
+ settingsMap:settingsMap
+ otrSettingsMap:offTheRecordSettingsMap]
+ attachSheetTo:[self window]];
+}
+
+- (void)setImagesEnabledIndex:(NSInteger)value {
+ ContentSetting setting = value == kContentSettingsEnabledIndex ?
+ CONTENT_SETTING_ALLOW : CONTENT_SETTING_BLOCK;
+ ContentSettingsDialogControllerInternal::PrefObserverDisabler
+ disabler(observer_.get());
+ profile_->GetHostContentSettingsMap()->SetDefaultContentSetting(
+ CONTENT_SETTINGS_TYPE_IMAGES, setting);
+}
+
+- (NSInteger)imagesEnabledIndex {
+ HostContentSettingsMap* settingsMap = profile_->GetHostContentSettingsMap();
+ bool enabled =
+ settingsMap->GetDefaultContentSetting(CONTENT_SETTINGS_TYPE_IMAGES) ==
+ CONTENT_SETTING_ALLOW;
+ return enabled ? kContentSettingsEnabledIndex : kContentSettingsDisabledIndex;
+}
+
+- (BOOL)imagesSettingsManaged {
+ return profile_->GetHostContentSettingsMap()->IsDefaultContentSettingManaged(
+ CONTENT_SETTINGS_TYPE_IMAGES);
+}
+
+- (void)setJavaScriptEnabledIndex:(NSInteger)value {
+ ContentSetting setting = value == kContentSettingsEnabledIndex ?
+ CONTENT_SETTING_ALLOW : CONTENT_SETTING_BLOCK;
+ ContentSettingsDialogControllerInternal::PrefObserverDisabler
+ disabler(observer_.get());
+ profile_->GetHostContentSettingsMap()->SetDefaultContentSetting(
+ CONTENT_SETTINGS_TYPE_JAVASCRIPT, setting);
+}
+
+- (NSInteger)javaScriptEnabledIndex {
+ HostContentSettingsMap* settingsMap = profile_->GetHostContentSettingsMap();
+ bool enabled =
+ settingsMap->GetDefaultContentSetting(CONTENT_SETTINGS_TYPE_JAVASCRIPT) ==
+ CONTENT_SETTING_ALLOW;
+ return enabled ? kContentSettingsEnabledIndex : kContentSettingsDisabledIndex;
+}
+
+- (BOOL)javaScriptSettingsManaged {
+ return profile_->GetHostContentSettingsMap()->IsDefaultContentSettingManaged(
+ CONTENT_SETTINGS_TYPE_JAVASCRIPT);
+}
+
+- (void)setPluginsEnabledIndex:(NSInteger)value {
+ ContentSetting setting = CONTENT_SETTING_DEFAULT;
+ switch (value) {
+ case kPluginsAllowIndex:
+ setting = CONTENT_SETTING_ALLOW;
+ break;
+ case kPluginsAskIndex:
+ setting = CONTENT_SETTING_ASK;
+ break;
+ case kPluginsBlockIndex:
+ setting = CONTENT_SETTING_BLOCK;
+ break;
+ default:
+ NOTREACHED();
+ }
+ ContentSettingsDialogControllerInternal::PrefObserverDisabler
+ disabler(observer_.get());
+ profile_->GetHostContentSettingsMap()->SetDefaultContentSetting(
+ CONTENT_SETTINGS_TYPE_PLUGINS, setting);
+}
+
+- (NSInteger)pluginsEnabledIndex {
+ HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
+ ContentSetting setting =
+ map->GetDefaultContentSetting(CONTENT_SETTINGS_TYPE_PLUGINS);
+ switch (setting) {
+ case CONTENT_SETTING_ALLOW:
+ return kPluginsAllowIndex;
+ case CONTENT_SETTING_ASK:
+ if (CommandLine::ForCurrentProcess()->HasSwitch(
+ switches::kEnableClickToPlay))
+ return kPluginsAskIndex;
+ // Fall through to the next case.
+ case CONTENT_SETTING_BLOCK:
+ return kPluginsBlockIndex;
+ default:
+ NOTREACHED();
+ return kPluginsAllowIndex;
+ }
+}
+
+- (BOOL)pluginsSettingsManaged {
+ return profile_->GetHostContentSettingsMap()->IsDefaultContentSettingManaged(
+ CONTENT_SETTINGS_TYPE_PLUGINS);
+}
+
+- (void)setPopupsEnabledIndex:(NSInteger)value {
+ ContentSetting setting = value == kContentSettingsEnabledIndex ?
+ CONTENT_SETTING_ALLOW : CONTENT_SETTING_BLOCK;
+ ContentSettingsDialogControllerInternal::PrefObserverDisabler
+ disabler(observer_.get());
+ profile_->GetHostContentSettingsMap()->SetDefaultContentSetting(
+ CONTENT_SETTINGS_TYPE_POPUPS, setting);
+}
+
+- (NSInteger)popupsEnabledIndex {
+ HostContentSettingsMap* settingsMap = profile_->GetHostContentSettingsMap();
+ bool enabled =
+ settingsMap->GetDefaultContentSetting(CONTENT_SETTINGS_TYPE_POPUPS) ==
+ CONTENT_SETTING_ALLOW;
+ return enabled ? kContentSettingsEnabledIndex : kContentSettingsDisabledIndex;
+}
+
+- (BOOL)popupsSettingsManaged {
+ return profile_->GetHostContentSettingsMap()->IsDefaultContentSettingManaged(
+ CONTENT_SETTINGS_TYPE_POPUPS);
+}
+
+- (void)setGeolocationSettingIndex:(NSInteger)value {
+ ContentSetting setting = CONTENT_SETTING_DEFAULT;
+ switch (value) {
+ case kGeolocationEnabledIndex: setting = CONTENT_SETTING_ALLOW; break;
+ case kGeolocationAskIndex: setting = CONTENT_SETTING_ASK; break;
+ case kGeolocationDisabledIndex: setting = CONTENT_SETTING_BLOCK; break;
+ default:
+ NOTREACHED();
+ }
+ ContentSettingsDialogControllerInternal::PrefObserverDisabler
+ disabler(observer_.get());
+ profile_->GetGeolocationContentSettingsMap()->SetDefaultContentSetting(
+ setting);
+}
+
+- (NSInteger)geolocationSettingIndex {
+ ContentSetting setting =
+ profile_->GetGeolocationContentSettingsMap()->GetDefaultContentSetting();
+ switch (setting) {
+ case CONTENT_SETTING_ALLOW: return kGeolocationEnabledIndex;
+ case CONTENT_SETTING_ASK: return kGeolocationAskIndex;
+ case CONTENT_SETTING_BLOCK: return kGeolocationDisabledIndex;
+ default:
+ NOTREACHED();
+ return kGeolocationAskIndex;
+ }
+}
+
+- (void)setNotificationsSettingIndex:(NSInteger)value {
+ ContentSetting setting = CONTENT_SETTING_DEFAULT;
+ switch (value) {
+ case kNotificationsEnabledIndex: setting = CONTENT_SETTING_ALLOW; break;
+ case kNotificationsAskIndex: setting = CONTENT_SETTING_ASK; break;
+ case kNotificationsDisabledIndex: setting = CONTENT_SETTING_BLOCK; break;
+ default:
+ NOTREACHED();
+ }
+ ContentSettingsDialogControllerInternal::PrefObserverDisabler
+ disabler(observer_.get());
+ profile_->GetDesktopNotificationService()->SetDefaultContentSetting(
+ setting);
+}
+
+- (NSInteger)notificationsSettingIndex {
+ ContentSetting setting =
+ profile_->GetDesktopNotificationService()->GetDefaultContentSetting();
+ switch (setting) {
+ case CONTENT_SETTING_ALLOW: return kNotificationsEnabledIndex;
+ case CONTENT_SETTING_ASK: return kNotificationsAskIndex;
+ case CONTENT_SETTING_BLOCK: return kNotificationsDisabledIndex;
+ default:
+ NOTREACHED();
+ return kGeolocationAskIndex;
+ }
+}
+
+// Callback when preferences are changed. |prefName| is the name of the
+// pref that has changed and should not be NULL.
+- (void)prefChanged:(const std::string&)prefName {
+ if (prefName == prefs::kClearSiteDataOnExit) {
+ [self willChangeValueForKey:@"clearSiteDataOnExit"];
+ [self didChangeValueForKey:@"clearSiteDataOnExit"];
+ }
+ if (prefName == prefs::kBlockThirdPartyCookies) {
+ [self willChangeValueForKey:@"blockThirdPartyCookies"];
+ [self didChangeValueForKey:@"blockThirdPartyCookies"];
+ [self willChangeValueForKey:@"blockThirdPartyCookiesManaged"];
+ [self didChangeValueForKey:@"blockThirdPartyCookiesManaged"];
+ }
+ if (prefName == prefs::kBlockNonsandboxedPlugins) {
+ [self willChangeValueForKey:@"pluginsEnabledIndex"];
+ [self didChangeValueForKey:@"pluginsEnabledIndex"];
+ }
+ if (prefName == prefs::kDefaultContentSettings) {
+ // We don't know exactly which setting has changed, so we'll tickle all
+ // of the properties that apply to kDefaultContentSettings. This will
+ // keep the UI up-to-date.
+ [self willChangeValueForKey:@"cookieSettingIndex"];
+ [self didChangeValueForKey:@"cookieSettingIndex"];
+ [self willChangeValueForKey:@"imagesEnabledIndex"];
+ [self didChangeValueForKey:@"imagesEnabledIndex"];
+ [self willChangeValueForKey:@"javaScriptEnabledIndex"];
+ [self didChangeValueForKey:@"javaScriptEnabledIndex"];
+ [self willChangeValueForKey:@"pluginsEnabledIndex"];
+ [self didChangeValueForKey:@"pluginsEnabledIndex"];
+ [self willChangeValueForKey:@"popupsEnabledIndex"];
+ [self didChangeValueForKey:@"popupsEnabledIndex"];
+
+ // Updates the "Enable" state of the radio groups and the exception buttons.
+ [self willChangeValueForKey:@"cookieSettingsManaged"];
+ [self didChangeValueForKey:@"cookieSettingsManaged"];
+ [self willChangeValueForKey:@"imagesSettingsManaged"];
+ [self didChangeValueForKey:@"imagesSettingsManaged"];
+ [self willChangeValueForKey:@"javaScriptSettingsManaged"];
+ [self didChangeValueForKey:@"javaScriptSettingsManaged"];
+ [self willChangeValueForKey:@"pluginsSettingsManaged"];
+ [self didChangeValueForKey:@"pluginsSettingsManaged"];
+ [self willChangeValueForKey:@"popupsSettingsManaged"];
+ [self didChangeValueForKey:@"popupsSettingsManaged"];
+ }
+ if (prefName == prefs::kGeolocationDefaultContentSetting) {
+ [self willChangeValueForKey:@"geolocationSettingIndex"];
+ [self didChangeValueForKey:@"geolocationSettingIndex"];
+ }
+ if (prefName == prefs::kDesktopNotificationDefaultContentSetting) {
+ [self willChangeValueForKey:@"notificationsSettingIndex"];
+ [self didChangeValueForKey:@"notificationsSettingIndex"];
+ }
+}
+
+- (void)contentSettingsChanged:
+ (HostContentSettingsMap::ContentSettingsDetails*)details {
+ [self prefChanged:prefs::kBlockNonsandboxedPlugins];
+ [self prefChanged:prefs::kDefaultContentSettings];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/content_settings_dialog_controller_unittest.mm b/chrome/browser/ui/cocoa/content_settings_dialog_controller_unittest.mm
new file mode 100644
index 0000000..1d48509
--- /dev/null
+++ b/chrome/browser/ui/cocoa/content_settings_dialog_controller_unittest.mm
@@ -0,0 +1,289 @@
+// 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/ui/cocoa/content_settings_dialog_controller.h"
+
+#include "base/auto_reset.h"
+#include "base/command_line.h"
+#import "base/scoped_nsobject.h"
+#include "base/ref_counted.h"
+#include "chrome/browser/content_settings/host_content_settings_map.h"
+#include "chrome/browser/geolocation/geolocation_content_settings_map.h"
+#include "chrome/browser/notifications/desktop_notification_service.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/chrome_switches.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class ContentSettingsDialogControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ TestingProfile* profile = browser_helper_.profile();
+ settingsMap_ = new HostContentSettingsMap(profile);
+ geoSettingsMap_ = new GeolocationContentSettingsMap(profile);
+ notificationsService_.reset(new DesktopNotificationService(profile, NULL));
+ controller_ = [ContentSettingsDialogController
+ showContentSettingsForType:CONTENT_SETTINGS_TYPE_DEFAULT
+ profile:browser_helper_.profile()];
+ }
+
+ virtual void TearDown() {
+ [controller_ close];
+ CocoaTest::TearDown();
+ }
+
+ protected:
+ ContentSettingsDialogController* controller_;
+ BrowserTestHelper browser_helper_;
+ scoped_refptr<HostContentSettingsMap> settingsMap_;
+ scoped_refptr<GeolocationContentSettingsMap> geoSettingsMap_;
+ scoped_ptr<DesktopNotificationService> notificationsService_;
+};
+
+// Test that +showContentSettingsDialogForProfile brings up the existing editor
+// and doesn't leak or crash.
+TEST_F(ContentSettingsDialogControllerTest, CreateDialog) {
+ EXPECT_TRUE(controller_);
+}
+
+TEST_F(ContentSettingsDialogControllerTest, CookieSetting) {
+ // Change setting, check dialog property.
+ settingsMap_->SetDefaultContentSetting(CONTENT_SETTINGS_TYPE_COOKIES,
+ CONTENT_SETTING_ALLOW);
+ EXPECT_EQ([controller_ cookieSettingIndex], kCookieEnabledIndex);
+
+ settingsMap_->SetDefaultContentSetting(CONTENT_SETTINGS_TYPE_COOKIES,
+ CONTENT_SETTING_BLOCK);
+ EXPECT_EQ([controller_ cookieSettingIndex], kCookieDisabledIndex);
+
+ // Change dialog property, check setting.
+ NSInteger setting;
+ [controller_ setCookieSettingIndex:kCookieEnabledIndex];
+ setting =
+ settingsMap_->GetDefaultContentSetting(CONTENT_SETTINGS_TYPE_COOKIES);
+ EXPECT_EQ(setting, CONTENT_SETTING_ALLOW);
+
+ [controller_ setCookieSettingIndex:kCookieDisabledIndex];
+ setting =
+ settingsMap_->GetDefaultContentSetting(CONTENT_SETTINGS_TYPE_COOKIES);
+ EXPECT_EQ(setting, CONTENT_SETTING_BLOCK);
+}
+
+TEST_F(ContentSettingsDialogControllerTest, BlockThirdPartyCookiesSetting) {
+ // Change setting, check dialog property.
+ settingsMap_->SetBlockThirdPartyCookies(YES);
+ EXPECT_TRUE([controller_ blockThirdPartyCookies]);
+
+ settingsMap_->SetBlockThirdPartyCookies(NO);
+ EXPECT_FALSE([controller_ blockThirdPartyCookies]);
+
+ // Change dialog property, check setting.
+ [controller_ setBlockThirdPartyCookies:YES];
+ EXPECT_TRUE(settingsMap_->BlockThirdPartyCookies());
+
+ [controller_ setBlockThirdPartyCookies:NO];
+ EXPECT_FALSE(settingsMap_->BlockThirdPartyCookies());
+}
+
+TEST_F(ContentSettingsDialogControllerTest, ClearSiteDataOnExitSetting) {
+ TestingProfile* profile = browser_helper_.profile();
+
+ // Change setting, check dialog property.
+ profile->GetPrefs()->SetBoolean(prefs::kClearSiteDataOnExit, true);
+ EXPECT_TRUE([controller_ clearSiteDataOnExit]);
+
+ profile->GetPrefs()->SetBoolean(prefs::kClearSiteDataOnExit, false);
+ EXPECT_FALSE([controller_ clearSiteDataOnExit]);
+
+ // Change dialog property, check setting.
+ [controller_ setClearSiteDataOnExit:YES];
+ EXPECT_TRUE(profile->GetPrefs()->GetBoolean(prefs::kClearSiteDataOnExit));
+
+ [controller_ setClearSiteDataOnExit:NO];
+ EXPECT_FALSE(profile->GetPrefs()->GetBoolean(prefs::kClearSiteDataOnExit));
+}
+
+TEST_F(ContentSettingsDialogControllerTest, ImagesSetting) {
+ // Change setting, check dialog property.
+ settingsMap_->SetDefaultContentSetting(CONTENT_SETTINGS_TYPE_IMAGES,
+ CONTENT_SETTING_ALLOW);
+ EXPECT_EQ([controller_ imagesEnabledIndex], kContentSettingsEnabledIndex);
+
+ settingsMap_->SetDefaultContentSetting(CONTENT_SETTINGS_TYPE_IMAGES,
+ CONTENT_SETTING_BLOCK);
+ EXPECT_EQ([controller_ imagesEnabledIndex], kContentSettingsDisabledIndex);
+
+ // Change dialog property, check setting.
+ NSInteger setting;
+ [controller_ setImagesEnabledIndex:kContentSettingsEnabledIndex];
+ setting =
+ settingsMap_->GetDefaultContentSetting(CONTENT_SETTINGS_TYPE_IMAGES);
+ EXPECT_EQ(setting, CONTENT_SETTING_ALLOW);
+
+ [controller_ setImagesEnabledIndex:kContentSettingsDisabledIndex];
+ setting =
+ settingsMap_->GetDefaultContentSetting(CONTENT_SETTINGS_TYPE_IMAGES);
+ EXPECT_EQ(setting, CONTENT_SETTING_BLOCK);
+}
+
+TEST_F(ContentSettingsDialogControllerTest, JavaScriptSetting) {
+ // Change setting, check dialog property.
+ settingsMap_->SetDefaultContentSetting(CONTENT_SETTINGS_TYPE_JAVASCRIPT,
+ CONTENT_SETTING_ALLOW);
+ EXPECT_EQ([controller_ javaScriptEnabledIndex], kContentSettingsEnabledIndex);
+
+ settingsMap_->SetDefaultContentSetting(CONTENT_SETTINGS_TYPE_JAVASCRIPT,
+ CONTENT_SETTING_BLOCK);
+ EXPECT_EQ([controller_ javaScriptEnabledIndex],
+ kContentSettingsDisabledIndex);
+
+ // Change dialog property, check setting.
+ NSInteger setting;
+ [controller_ setJavaScriptEnabledIndex:kContentSettingsEnabledIndex];
+ setting =
+ settingsMap_->GetDefaultContentSetting(CONTENT_SETTINGS_TYPE_JAVASCRIPT);
+ EXPECT_EQ(setting, CONTENT_SETTING_ALLOW);
+
+ [controller_ setJavaScriptEnabledIndex:kContentSettingsDisabledIndex];
+ setting =
+ settingsMap_->GetDefaultContentSetting(CONTENT_SETTINGS_TYPE_JAVASCRIPT);
+ EXPECT_EQ(setting, CONTENT_SETTING_BLOCK);
+}
+
+TEST_F(ContentSettingsDialogControllerTest, PluginsSetting) {
+ // Change setting, check dialog property.
+ settingsMap_->SetDefaultContentSetting(CONTENT_SETTINGS_TYPE_PLUGINS,
+ CONTENT_SETTING_ALLOW);
+ EXPECT_EQ(kPluginsAllowIndex, [controller_ pluginsEnabledIndex]);
+
+ settingsMap_->SetDefaultContentSetting(CONTENT_SETTINGS_TYPE_PLUGINS,
+ CONTENT_SETTING_BLOCK);
+ EXPECT_EQ(kPluginsBlockIndex, [controller_ pluginsEnabledIndex]);
+
+ {
+ // Click-to-play needs to be enabled to set the content setting to ASK.
+ CommandLine* cmd = CommandLine::ForCurrentProcess();
+ AutoReset<CommandLine> auto_reset(cmd, *cmd);
+ cmd->AppendSwitch(switches::kEnableClickToPlay);
+
+ settingsMap_->SetDefaultContentSetting(CONTENT_SETTINGS_TYPE_PLUGINS,
+ CONTENT_SETTING_ASK);
+ EXPECT_EQ(kPluginsAskIndex, [controller_ pluginsEnabledIndex]);
+ }
+
+ // Change dialog property, check setting.
+ NSInteger setting;
+ [controller_ setPluginsEnabledIndex:kPluginsAllowIndex];
+ setting =
+ settingsMap_->GetDefaultContentSetting(CONTENT_SETTINGS_TYPE_PLUGINS);
+ EXPECT_EQ(CONTENT_SETTING_ALLOW, setting);
+
+ [controller_ setPluginsEnabledIndex:kPluginsBlockIndex];
+ setting =
+ settingsMap_->GetDefaultContentSetting(CONTENT_SETTINGS_TYPE_PLUGINS);
+ EXPECT_EQ(CONTENT_SETTING_BLOCK, setting);
+
+ {
+ CommandLine* cmd = CommandLine::ForCurrentProcess();
+ AutoReset<CommandLine> auto_reset(cmd, *cmd);
+ cmd->AppendSwitch(switches::kEnableClickToPlay);
+
+ [controller_ setPluginsEnabledIndex:kPluginsAskIndex];
+ setting =
+ settingsMap_->GetDefaultContentSetting(CONTENT_SETTINGS_TYPE_PLUGINS);
+ EXPECT_EQ(CONTENT_SETTING_ASK, setting);
+ }
+}
+
+TEST_F(ContentSettingsDialogControllerTest, PopupsSetting) {
+ // Change setting, check dialog property.
+ settingsMap_->SetDefaultContentSetting(CONTENT_SETTINGS_TYPE_POPUPS,
+ CONTENT_SETTING_ALLOW);
+ EXPECT_EQ([controller_ popupsEnabledIndex], kContentSettingsEnabledIndex);
+
+ settingsMap_->SetDefaultContentSetting(CONTENT_SETTINGS_TYPE_POPUPS,
+ CONTENT_SETTING_BLOCK);
+ EXPECT_EQ([controller_ popupsEnabledIndex], kContentSettingsDisabledIndex);
+
+ // Change dialog property, check setting.
+ NSInteger setting;
+ [controller_ setPopupsEnabledIndex:kContentSettingsEnabledIndex];
+ setting =
+ settingsMap_->GetDefaultContentSetting(CONTENT_SETTINGS_TYPE_POPUPS);
+ EXPECT_EQ(setting, CONTENT_SETTING_ALLOW);
+
+ [controller_ setPopupsEnabledIndex:kContentSettingsDisabledIndex];
+ setting =
+ settingsMap_->GetDefaultContentSetting(CONTENT_SETTINGS_TYPE_POPUPS);
+ EXPECT_EQ(setting, CONTENT_SETTING_BLOCK);
+}
+
+TEST_F(ContentSettingsDialogControllerTest, GeolocationSetting) {
+ // Change setting, check dialog property.
+ geoSettingsMap_->SetDefaultContentSetting(CONTENT_SETTING_ALLOW);
+ EXPECT_EQ([controller_ geolocationSettingIndex], kGeolocationEnabledIndex);
+
+ geoSettingsMap_->SetDefaultContentSetting(CONTENT_SETTING_ASK);
+ EXPECT_EQ([controller_ geolocationSettingIndex], kGeolocationAskIndex);
+
+ geoSettingsMap_->SetDefaultContentSetting(CONTENT_SETTING_BLOCK);
+ EXPECT_EQ([controller_ geolocationSettingIndex], kGeolocationDisabledIndex);
+
+ // Change dialog property, check setting.
+ NSInteger setting;
+ [controller_ setGeolocationSettingIndex:kGeolocationEnabledIndex];
+ setting =
+ geoSettingsMap_->GetDefaultContentSetting();
+ EXPECT_EQ(setting, CONTENT_SETTING_ALLOW);
+
+ [controller_ setGeolocationSettingIndex:kGeolocationAskIndex];
+ setting =
+ geoSettingsMap_->GetDefaultContentSetting();
+ EXPECT_EQ(setting, CONTENT_SETTING_ASK);
+
+ [controller_ setGeolocationSettingIndex:kGeolocationDisabledIndex];
+ setting =
+ geoSettingsMap_->GetDefaultContentSetting();
+ EXPECT_EQ(setting, CONTENT_SETTING_BLOCK);
+}
+
+TEST_F(ContentSettingsDialogControllerTest, NotificationsSetting) {
+ // Change setting, check dialog property.
+ notificationsService_->SetDefaultContentSetting(CONTENT_SETTING_ALLOW);
+ EXPECT_EQ([controller_ notificationsSettingIndex],
+ kNotificationsEnabledIndex);
+
+ notificationsService_->SetDefaultContentSetting(CONTENT_SETTING_ASK);
+ EXPECT_EQ([controller_ notificationsSettingIndex], kNotificationsAskIndex);
+
+ notificationsService_->SetDefaultContentSetting(CONTENT_SETTING_BLOCK);
+ EXPECT_EQ([controller_ notificationsSettingIndex],
+ kNotificationsDisabledIndex);
+
+ // Change dialog property, check setting.
+ NSInteger setting;
+ [controller_ setNotificationsSettingIndex:kNotificationsEnabledIndex];
+ setting =
+ notificationsService_->GetDefaultContentSetting();
+ EXPECT_EQ(setting, CONTENT_SETTING_ALLOW);
+
+ [controller_ setNotificationsSettingIndex:kNotificationsAskIndex];
+ setting =
+ notificationsService_->GetDefaultContentSetting();
+ EXPECT_EQ(setting, CONTENT_SETTING_ASK);
+
+ [controller_ setNotificationsSettingIndex:kNotificationsDisabledIndex];
+ setting =
+ notificationsService_->GetDefaultContentSetting();
+ EXPECT_EQ(setting, CONTENT_SETTING_BLOCK);
+}
+
+} // namespace
+
diff --git a/chrome/browser/ui/cocoa/cookie_details.h b/chrome/browser/ui/cocoa/cookie_details.h
new file mode 100644
index 0000000..614c87c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/cookie_details.h
@@ -0,0 +1,224 @@
+// 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 "chrome/browser/browsing_data_database_helper.h"
+#include "chrome/browser/browsing_data_indexed_db_helper.h"
+#include "chrome/browser/browsing_data_local_storage_helper.h"
+#include "base/scoped_nsobject.h"
+#include "net/base/cookie_monster.h"
+#include "webkit/appcache/appcache_service.h"
+
+class CookieTreeNode;
+class CookiePromptModalDialog;
+
+// This enum specifies the type of information contained in the
+// cookie details.
+enum CocoaCookieDetailsType {
+ // Represents grouping of cookie data, used in the cookie tree.
+ kCocoaCookieDetailsTypeFolder = 0,
+
+ // Detailed information about a cookie, used both in the cookie
+ // tree and the cookie prompt.
+ kCocoaCookieDetailsTypeCookie,
+
+ // Detailed information about a web database used for
+ // display in the cookie tree.
+ kCocoaCookieDetailsTypeTreeDatabase,
+
+ // Detailed information about local storage used for
+ // display in the cookie tree.
+ kCocoaCookieDetailsTypeTreeLocalStorage,
+
+ // Detailed information about an appcache used for display in the
+ // cookie tree.
+ kCocoaCookieDetailsTypeTreeAppCache,
+
+ // Detailed information about an IndexedDB used for display in the
+ // cookie tree.
+ kCocoaCookieDetailsTypeTreeIndexedDB,
+
+ // Detailed information about a web database used for display
+ // in the cookie prompt dialog.
+ kCocoaCookieDetailsTypePromptDatabase,
+
+ // Detailed information about local storage used for display
+ // in the cookie prompt dialog.
+ kCocoaCookieDetailsTypePromptLocalStorage,
+
+ // Detailed information about app caches used for display
+ // in the cookie prompt dialog.
+ kCocoaCookieDetailsTypePromptAppCache
+};
+
+// This class contains all of the information that can be displayed in
+// a cookie details view. Because the view uses bindings to display
+// the cookie information, the methods that provide that information
+// for display must be implemented directly on this class and not on any
+// of its subclasses.
+// If this system is rewritten to not use bindings, this class should be
+// subclassed and specialized, rather than using an enum to determine type.
+@interface CocoaCookieDetails : NSObject {
+ @private
+ CocoaCookieDetailsType type_;
+
+ // Used for type kCocoaCookieDetailsTypeCookie to indicate whether
+ // it should be possible to edit the expiration.
+ BOOL canEditExpiration_;
+
+ // Indicates whether a cookie has an explcit expiration. If not
+ // it will expire with the session.
+ BOOL hasExpiration_;
+
+ // Only set for type kCocoaCookieDetailsTypeCookie.
+ scoped_nsobject<NSString> content_;
+ scoped_nsobject<NSString> path_;
+ scoped_nsobject<NSString> sendFor_;
+ // Stringifed dates.
+ scoped_nsobject<NSString> expires_;
+
+ // Only set for type kCocoaCookieDetailsTypeCookie and
+ // kCocoaCookieDetailsTypeTreeAppCache nodes.
+ scoped_nsobject<NSString> created_;
+
+ // Only set for types kCocoaCookieDetailsTypeCookie, and
+ // kCocoaCookieDetailsTypePromptDatabase nodes.
+ scoped_nsobject<NSString> name_;
+
+ // Only set for type kCocoaCookieDetailsTypeTreeLocalStorage,
+ // kCocoaCookieDetailsTypeTreeDatabase,
+ // kCocoaCookieDetailsTypePromptDatabase,
+ // kCocoaCookieDetailsTypeTreeIndexedDB, and
+ // kCocoaCookieDetailsTypeTreeAppCache nodes.
+ scoped_nsobject<NSString> fileSize_;
+
+ // Only set for types kCocoaCookieDetailsTypeTreeLocalStorage,
+ // kCocoaCookieDetailsTypeTreeDatabase, and
+ // kCocoaCookieDetailsTypeTreeIndexedDB nodes.
+ scoped_nsobject<NSString> lastModified_;
+
+ // Only set for type kCocoaCookieDetailsTypeTreeAppCache nodes.
+ scoped_nsobject<NSString> lastAccessed_;
+
+ // Only set for type kCocoaCookieDetailsTypeCookie,
+ // kCocoaCookieDetailsTypePromptDatabase,
+ // kCocoaCookieDetailsTypePromptLocalStorage, and
+ // kCocoaCookieDetailsTypeTreeIndexedDB nodes.
+ scoped_nsobject<NSString> domain_;
+
+ // Only set for type kCocoaCookieTreeNodeTypeDatabaseStorage and
+ // kCocoaCookieDetailsTypePromptDatabase nodes.
+ scoped_nsobject<NSString> databaseDescription_;
+
+ // Only set for type kCocoaCookieDetailsTypePromptLocalStorage.
+ scoped_nsobject<NSString> localStorageKey_;
+ scoped_nsobject<NSString> localStorageValue_;
+
+ // Only set for type kCocoaCookieDetailsTypeTreeAppCache and
+ // kCocoaCookieDetailsTypePromptAppCache.
+ scoped_nsobject<NSString> manifestURL_;
+}
+
+@property (nonatomic, readonly) BOOL canEditExpiration;
+@property (nonatomic) BOOL hasExpiration;
+@property (nonatomic, readonly) CocoaCookieDetailsType type;
+
+// The following methods are used in the bindings of subviews inside
+// the cookie detail view. Note that the method that tests the
+// visibility of the subview for cookie-specific information has a different
+// polarity than the other visibility testing methods. This ensures that
+// this subview is shown when there is no selection in the cookie tree,
+// because a hidden value of |false| is generated when the key value binding
+// is evaluated through a nil object. The other methods are bound using a
+// |NSNegateBoolean| transformer, so that when there is a empty selection the
+// hidden value is |true|.
+- (BOOL)shouldHideCookieDetailsView;
+- (BOOL)shouldShowLocalStorageTreeDetailsView;
+- (BOOL)shouldShowLocalStoragePromptDetailsView;
+- (BOOL)shouldShowDatabaseTreeDetailsView;
+- (BOOL)shouldShowDatabasePromptDetailsView;
+- (BOOL)shouldShowAppCachePromptDetailsView;
+- (BOOL)shouldShowAppCacheTreeDetailsView;
+- (BOOL)shouldShowIndexedDBTreeDetailsView;
+
+- (NSString*)name;
+- (NSString*)content;
+- (NSString*)domain;
+- (NSString*)path;
+- (NSString*)sendFor;
+- (NSString*)created;
+- (NSString*)expires;
+- (NSString*)fileSize;
+- (NSString*)lastModified;
+- (NSString*)lastAccessed;
+- (NSString*)databaseDescription;
+- (NSString*)localStorageKey;
+- (NSString*)localStorageValue;
+- (NSString*)manifestURL;
+
+// Used for folders in the cookie tree.
+- (id)initAsFolder;
+
+// Used for cookie details in both the cookie tree and the cookie prompt dialog.
+- (id)initWithCookie:(const net::CookieMonster::CanonicalCookie*)treeNode
+ origin:(NSString*)origin
+ canEditExpiration:(BOOL)canEditExpiration;
+
+// Used for database details in the cookie tree.
+- (id)initWithDatabase:
+ (const BrowsingDataDatabaseHelper::DatabaseInfo*)databaseInfo;
+
+// Used for local storage details in the cookie tree.
+- (id)initWithLocalStorage:
+ (const BrowsingDataLocalStorageHelper::LocalStorageInfo*)localStorageInfo;
+
+// Used for database details in the cookie prompt dialog.
+- (id)initWithDatabase:(const std::string&)domain
+ databaseName:(const string16&)databaseName
+ databaseDescription:(const string16&)databaseDescription
+ fileSize:(unsigned long)fileSize;
+
+// -initWithAppCacheInfo: creates a cookie details with the manifest URL plus
+// all of this additional information that is available after an appcache is
+// actually created, including it's creation date, size and last accessed time.
+- (id)initWithAppCacheInfo:(const appcache::AppCacheInfo*)appcacheInfo;
+
+// Used for local storage details in the cookie prompt dialog.
+- (id)initWithLocalStorage:(const std::string&)domain
+ key:(const string16&)key
+ value:(const string16&)value;
+
+// -initWithAppCacheManifestURL: is called when the cookie prompt is displayed
+// for an appcache, at that time only the manifest URL of the appcache is known.
+- (id)initWithAppCacheManifestURL:(const std::string&)manifestURL;
+
+// Used for IndexedDB details in the cookie tree.
+- (id)initWithIndexedDBInfo:
+ (const BrowsingDataIndexedDBHelper::IndexedDBInfo*)indexedDB;
+
+// A factory method to create a configured instance given a node from
+// the cookie tree in |treeNode|.
++ (CocoaCookieDetails*)createFromCookieTreeNode:(CookieTreeNode*)treeNode;
+
+@end
+
+// The subpanes of the cookie details view expect to be able to bind to methods
+// through a key path in the form |content.details.xxxx|. This class serves as
+// an adapter that simply wraps a |CocoaCookieDetails| object. An instance of
+// this class is set as the content object for cookie details view's object
+// controller so that key paths are properly resolved through to the
+// |CocoaCookieDetails| object for the cookie prompt.
+@interface CookiePromptContentDetailsAdapter : NSObject {
+ @private
+ scoped_nsobject<CocoaCookieDetails> details_;
+}
+
+- (CocoaCookieDetails*)details;
+
+// The adapter assumes ownership of the details object
+// in its initializer.
+- (id)initWithDetails:(CocoaCookieDetails*)details;
+@end
+
diff --git a/chrome/browser/ui/cocoa/cookie_details.mm b/chrome/browser/ui/cocoa/cookie_details.mm
new file mode 100644
index 0000000..c6f5cec
--- /dev/null
+++ b/chrome/browser/ui/cocoa/cookie_details.mm
@@ -0,0 +1,299 @@
+// 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 "chrome/browser/ui/cocoa/cookie_details.h"
+
+#include "app/l10n_util_mac.h"
+#import "base/i18n/time_formatting.h"
+#include "base/sys_string_conversions.h"
+#include "grit/generated_resources.h"
+#include "chrome/browser/cookies_tree_model.h"
+#include "webkit/appcache/appcache_service.h"
+
+#pragma mark Cocoa Cookie Details
+
+@implementation CocoaCookieDetails
+
+@synthesize canEditExpiration = canEditExpiration_;
+@synthesize hasExpiration = hasExpiration_;
+@synthesize type = type_;
+
+- (BOOL)shouldHideCookieDetailsView {
+ return type_ != kCocoaCookieDetailsTypeFolder &&
+ type_ != kCocoaCookieDetailsTypeCookie;
+}
+
+- (BOOL)shouldShowLocalStorageTreeDetailsView {
+ return type_ == kCocoaCookieDetailsTypeTreeLocalStorage;
+}
+
+- (BOOL)shouldShowLocalStoragePromptDetailsView {
+ return type_ == kCocoaCookieDetailsTypePromptLocalStorage;
+}
+
+- (BOOL)shouldShowDatabaseTreeDetailsView {
+ return type_ == kCocoaCookieDetailsTypeTreeDatabase;
+}
+
+- (BOOL)shouldShowAppCacheTreeDetailsView {
+ return type_ == kCocoaCookieDetailsTypeTreeAppCache;
+}
+
+- (BOOL)shouldShowDatabasePromptDetailsView {
+ return type_ == kCocoaCookieDetailsTypePromptDatabase;
+}
+
+- (BOOL)shouldShowAppCachePromptDetailsView {
+ return type_ == kCocoaCookieDetailsTypePromptAppCache;
+}
+
+- (BOOL)shouldShowIndexedDBTreeDetailsView {
+ return type_ == kCocoaCookieDetailsTypeTreeIndexedDB;
+}
+
+- (NSString*)name {
+ return name_.get();
+}
+
+- (NSString*)content {
+ return content_.get();
+}
+
+- (NSString*)domain {
+ return domain_.get();
+}
+
+- (NSString*)path {
+ return path_.get();
+}
+
+- (NSString*)sendFor {
+ return sendFor_.get();
+}
+
+- (NSString*)created {
+ return created_.get();
+}
+
+- (NSString*)expires {
+ return expires_.get();
+}
+
+- (NSString*)fileSize {
+ return fileSize_.get();
+}
+
+- (NSString*)lastModified {
+ return lastModified_.get();
+}
+
+- (NSString*)lastAccessed {
+ return lastAccessed_.get();
+}
+
+- (NSString*)databaseDescription {
+ return databaseDescription_.get();
+}
+
+- (NSString*)localStorageKey {
+ return localStorageKey_.get();
+}
+
+- (NSString*)localStorageValue {
+ return localStorageValue_.get();
+}
+
+- (NSString*)manifestURL {
+ return manifestURL_.get();
+}
+
+- (id)initAsFolder {
+ if ((self = [super init])) {
+ type_ = kCocoaCookieDetailsTypeFolder;
+ }
+ return self;
+}
+
+- (id)initWithCookie:(const net::CookieMonster::CanonicalCookie*)cookie
+ origin:(NSString*)origin
+ canEditExpiration:(BOOL)canEditExpiration {
+ if ((self = [super init])) {
+ type_ = kCocoaCookieDetailsTypeCookie;
+ hasExpiration_ = cookie->DoesExpire();
+ canEditExpiration_ = canEditExpiration && hasExpiration_;
+ name_.reset([base::SysUTF8ToNSString(cookie->Name()) retain]);
+ content_.reset([base::SysUTF8ToNSString(cookie->Value()) retain]);
+ path_.reset([base::SysUTF8ToNSString(cookie->Path()) retain]);
+ domain_.reset([origin retain]);
+
+ if (cookie->DoesExpire()) {
+ expires_.reset([base::SysWideToNSString(
+ base::TimeFormatFriendlyDateAndTime(cookie->ExpiryDate())) retain]);
+ } else {
+ expires_.reset([l10n_util::GetNSStringWithFixup(
+ IDS_COOKIES_COOKIE_EXPIRES_SESSION) retain]);
+ }
+
+ created_.reset([base::SysWideToNSString(
+ base::TimeFormatFriendlyDateAndTime(cookie->CreationDate())) retain]);
+
+ if (cookie->IsSecure()) {
+ sendFor_.reset([l10n_util::GetNSStringWithFixup(
+ IDS_COOKIES_COOKIE_SENDFOR_SECURE) retain]);
+ } else {
+ sendFor_.reset([l10n_util::GetNSStringWithFixup(
+ IDS_COOKIES_COOKIE_SENDFOR_ANY) retain]);
+ }
+ }
+ return self;
+}
+
+- (id)initWithDatabase:(const BrowsingDataDatabaseHelper::DatabaseInfo*)
+ databaseInfo {
+ if ((self = [super init])) {
+ type_ = kCocoaCookieDetailsTypeTreeDatabase;
+ canEditExpiration_ = NO;
+ databaseDescription_.reset([base::SysUTF8ToNSString(
+ databaseInfo->description) retain]);
+ fileSize_.reset([base::SysUTF16ToNSString(FormatBytes(databaseInfo->size,
+ GetByteDisplayUnits(databaseInfo->size), true)) retain]);
+ lastModified_.reset([base::SysWideToNSString(
+ base::TimeFormatFriendlyDateAndTime(
+ databaseInfo->last_modified)) retain]);
+ }
+ return self;
+}
+
+- (id)initWithLocalStorage:(
+ const BrowsingDataLocalStorageHelper::LocalStorageInfo*)storageInfo {
+ if ((self = [super init])) {
+ type_ = kCocoaCookieDetailsTypeTreeLocalStorage;
+ canEditExpiration_ = NO;
+ domain_.reset([base::SysUTF8ToNSString(storageInfo->origin) retain]);
+ fileSize_.reset([base::SysUTF16ToNSString(FormatBytes(storageInfo->size,
+ GetByteDisplayUnits(storageInfo->size), true)) retain]);
+ lastModified_.reset([base::SysWideToNSString(
+ base::TimeFormatFriendlyDateAndTime(
+ storageInfo->last_modified)) retain]);
+ }
+ return self;
+}
+
+- (id)initWithAppCacheInfo:(const appcache::AppCacheInfo*)appcacheInfo {
+ if ((self = [super init])) {
+ type_ = kCocoaCookieDetailsTypeTreeAppCache;
+ canEditExpiration_ = NO;
+ manifestURL_.reset([base::SysUTF8ToNSString(
+ appcacheInfo->manifest_url.spec()) retain]);
+ fileSize_.reset([base::SysUTF16ToNSString(FormatBytes(appcacheInfo->size,
+ GetByteDisplayUnits(appcacheInfo->size), true)) retain]);
+ created_.reset([base::SysWideToNSString(
+ base::TimeFormatFriendlyDateAndTime(
+ appcacheInfo->creation_time)) retain]);
+ lastAccessed_.reset([base::SysWideToNSString(
+ base::TimeFormatFriendlyDateAndTime(
+ appcacheInfo->last_access_time)) retain]);
+ }
+ return self;
+}
+
+- (id)initWithDatabase:(const std::string&)domain
+ databaseName:(const string16&)databaseName
+ databaseDescription:(const string16&)databaseDescription
+ fileSize:(unsigned long)fileSize {
+ if ((self = [super init])) {
+ type_ = kCocoaCookieDetailsTypePromptDatabase;
+ canEditExpiration_ = NO;
+ name_.reset([base::SysUTF16ToNSString(databaseName) retain]);
+ domain_.reset([base::SysUTF8ToNSString(domain) retain]);
+ databaseDescription_.reset(
+ [base::SysUTF16ToNSString(databaseDescription) retain]);
+ fileSize_.reset([base::SysUTF16ToNSString(FormatBytes(fileSize,
+ GetByteDisplayUnits(fileSize), true)) retain]);
+ }
+ return self;
+}
+
+- (id)initWithLocalStorage:(const std::string&)domain
+ key:(const string16&)key
+ value:(const string16&)value {
+ if ((self = [super init])) {
+ type_ = kCocoaCookieDetailsTypePromptLocalStorage;
+ canEditExpiration_ = NO;
+ domain_.reset([base::SysUTF8ToNSString(domain) retain]);
+ localStorageKey_.reset([base::SysUTF16ToNSString(key) retain]);
+ localStorageValue_.reset([base::SysUTF16ToNSString(value) retain]);
+ }
+ return self;
+}
+
+- (id)initWithAppCacheManifestURL:(const std::string&)manifestURL {
+ if ((self = [super init])) {
+ type_ = kCocoaCookieDetailsTypePromptAppCache;
+ canEditExpiration_ = NO;
+ manifestURL_.reset([base::SysUTF8ToNSString(manifestURL) retain]);
+ }
+ return self;
+}
+
+- (id)initWithIndexedDBInfo:
+ (const BrowsingDataIndexedDBHelper::IndexedDBInfo*)indexedDBInfo {
+ if ((self = [super init])) {
+ type_ = kCocoaCookieDetailsTypeTreeIndexedDB;
+ canEditExpiration_ = NO;
+ domain_.reset([base::SysUTF8ToNSString(indexedDBInfo->origin) retain]);
+ fileSize_.reset([base::SysUTF16ToNSString(FormatBytes(indexedDBInfo->size,
+ GetByteDisplayUnits(indexedDBInfo->size), true)) retain]);
+ lastModified_.reset([base::SysWideToNSString(
+ base::TimeFormatFriendlyDateAndTime(
+ indexedDBInfo->last_modified)) retain]);
+ }
+ return self;
+}
+
++ (CocoaCookieDetails*)createFromCookieTreeNode:(CookieTreeNode*)treeNode {
+ CookieTreeNode::DetailedInfo info = treeNode->GetDetailedInfo();
+ CookieTreeNode::DetailedInfo::NodeType nodeType = info.node_type;
+ NSString* origin;
+ switch (nodeType) {
+ case CookieTreeNode::DetailedInfo::TYPE_COOKIE:
+ origin = base::SysWideToNSString(info.origin.c_str());
+ return [[[CocoaCookieDetails alloc] initWithCookie:info.cookie
+ origin:origin
+ canEditExpiration:NO] autorelease];
+ case CookieTreeNode::DetailedInfo::TYPE_DATABASE:
+ return [[[CocoaCookieDetails alloc]
+ initWithDatabase:info.database_info] autorelease];
+ case CookieTreeNode::DetailedInfo::TYPE_LOCAL_STORAGE:
+ return [[[CocoaCookieDetails alloc]
+ initWithLocalStorage:info.local_storage_info] autorelease];
+ case CookieTreeNode::DetailedInfo::TYPE_APPCACHE:
+ return [[[CocoaCookieDetails alloc]
+ initWithAppCacheInfo:info.appcache_info] autorelease];
+ case CookieTreeNode::DetailedInfo::TYPE_INDEXED_DB:
+ return [[[CocoaCookieDetails alloc]
+ initWithIndexedDBInfo:info.indexed_db_info] autorelease];
+ default:
+ return [[[CocoaCookieDetails alloc] initAsFolder] autorelease];
+ }
+}
+
+@end
+
+#pragma mark Content Object Adapter
+
+@implementation CookiePromptContentDetailsAdapter
+
+- (id)initWithDetails:(CocoaCookieDetails*)details {
+ if ((self = [super init])) {
+ details_.reset([details retain]);
+ }
+ return self;
+}
+
+- (CocoaCookieDetails*)details {
+ return details_.get();
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/cookie_details_unittest.mm b/chrome/browser/ui/cocoa/cookie_details_unittest.mm
new file mode 100644
index 0000000..0f7d711
--- /dev/null
+++ b/chrome/browser/ui/cocoa/cookie_details_unittest.mm
@@ -0,0 +1,247 @@
+// 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/sys_string_conversions.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/cookie_details.h"
+#include "googleurl/src/gurl.h"
+#import "testing/gtest_mac.h"
+
+namespace {
+
+class CookiesDetailsTest : public CocoaTest {
+};
+
+TEST_F(CookiesDetailsTest, CreateForFolder) {
+ scoped_nsobject<CocoaCookieDetails> details;
+ details.reset([[CocoaCookieDetails alloc] initAsFolder]);
+
+ EXPECT_EQ([details.get() type], kCocoaCookieDetailsTypeFolder);
+}
+
+TEST_F(CookiesDetailsTest, CreateForCookie) {
+ scoped_nsobject<CocoaCookieDetails> details;
+ GURL url("http://chromium.org");
+ std::string cookieLine(
+ "PHPSESSID=0123456789abcdef0123456789abcdef; path=/");
+ net::CookieMonster::ParsedCookie pc(cookieLine);
+ net::CookieMonster::CanonicalCookie cookie(url, pc);
+ NSString* origin = base::SysUTF8ToNSString("http://chromium.org");
+ details.reset([[CocoaCookieDetails alloc] initWithCookie:&cookie
+ origin:origin
+ canEditExpiration:NO]);
+
+ EXPECT_EQ([details.get() type], kCocoaCookieDetailsTypeCookie);
+ EXPECT_NSEQ(@"PHPSESSID", [details.get() name]);
+ EXPECT_NSEQ(@"0123456789abcdef0123456789abcdef",
+ [details.get() content]);
+ EXPECT_NSEQ(@"http://chromium.org", [details.get() domain]);
+ EXPECT_NSEQ(@"/", [details.get() path]);
+ EXPECT_NSNE(@"", [details.get() lastModified]);
+ EXPECT_NSNE(@"", [details.get() created]);
+ EXPECT_NSNE(@"", [details.get() sendFor]);
+
+ EXPECT_FALSE([details.get() shouldHideCookieDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowLocalStorageTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowDatabaseTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowAppCacheTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowIndexedDBTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowLocalStoragePromptDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowDatabasePromptDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowAppCachePromptDetailsView]);
+}
+
+TEST_F(CookiesDetailsTest, CreateForTreeDatabase) {
+ scoped_nsobject<CocoaCookieDetails> details;
+ std::string host("http://chromium.org");
+ std::string database_name("sassolungo");
+ std::string origin_identifier("dolomites");
+ std::string description("a great place to climb");
+ int64 size = 1234;
+ base::Time last_modified = base::Time::Now();
+ BrowsingDataDatabaseHelper::DatabaseInfo info(host, database_name,
+ origin_identifier, description, host, size, last_modified);
+ details.reset([[CocoaCookieDetails alloc] initWithDatabase:&info]);
+
+ EXPECT_EQ([details.get() type], kCocoaCookieDetailsTypeTreeDatabase);
+ EXPECT_NSEQ(@"a great place to climb", [details.get() databaseDescription]);
+ EXPECT_NSEQ(@"1234 B", [details.get() fileSize]);
+ EXPECT_NSNE(@"", [details.get() lastModified]);
+
+ EXPECT_TRUE([details.get() shouldHideCookieDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowLocalStorageTreeDetailsView]);
+ EXPECT_TRUE([details.get() shouldShowDatabaseTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowAppCacheTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowIndexedDBTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowLocalStoragePromptDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowDatabasePromptDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowAppCachePromptDetailsView]);
+}
+
+TEST_F(CookiesDetailsTest, CreateForTreeLocalStorage) {
+ scoped_nsobject<CocoaCookieDetails> details;
+ std::string protocol("http");
+ std::string host("chromium.org");
+ unsigned short port = 80;
+ std::string database_identifier("id");
+ std::string origin("chromium.org");
+ FilePath file_path(FILE_PATH_LITERAL("/"));
+ int64 size = 1234;
+ base::Time last_modified = base::Time::Now();
+ BrowsingDataLocalStorageHelper::LocalStorageInfo info(protocol, host, port,
+ database_identifier, origin, file_path, size, last_modified);
+ details.reset([[CocoaCookieDetails alloc] initWithLocalStorage:&info]);
+
+ EXPECT_EQ([details.get() type], kCocoaCookieDetailsTypeTreeLocalStorage);
+ EXPECT_NSEQ(@"chromium.org", [details.get() domain]);
+ EXPECT_NSEQ(@"1234 B", [details.get() fileSize]);
+ EXPECT_NSNE(@"", [details.get() lastModified]);
+
+ EXPECT_TRUE([details.get() shouldHideCookieDetailsView]);
+ EXPECT_TRUE([details.get() shouldShowLocalStorageTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowDatabaseTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowAppCacheTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowIndexedDBTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowLocalStoragePromptDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowDatabasePromptDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowAppCachePromptDetailsView]);
+}
+
+TEST_F(CookiesDetailsTest, CreateForTreeAppCache) {
+ scoped_nsobject<CocoaCookieDetails> details;
+
+ GURL url("http://chromium.org/stuff.manifest");
+ appcache::AppCacheInfo info;
+ info.creation_time = base::Time::Now();
+ info.last_update_time = base::Time::Now();
+ info.last_access_time = base::Time::Now();
+ info.size=2678;
+ info.manifest_url = url;
+ details.reset([[CocoaCookieDetails alloc] initWithAppCacheInfo:&info]);
+
+ EXPECT_EQ([details.get() type], kCocoaCookieDetailsTypeTreeAppCache);
+ EXPECT_NSEQ(@"http://chromium.org/stuff.manifest",
+ [details.get() manifestURL]);
+ EXPECT_NSEQ(@"2678 B", [details.get() fileSize]);
+ EXPECT_NSNE(@"", [details.get() lastAccessed]);
+ EXPECT_NSNE(@"", [details.get() created]);
+
+ EXPECT_TRUE([details.get() shouldHideCookieDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowLocalStorageTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowDatabaseTreeDetailsView]);
+ EXPECT_TRUE([details.get() shouldShowAppCacheTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowIndexedDBTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowLocalStoragePromptDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowDatabasePromptDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowAppCachePromptDetailsView]);
+}
+
+TEST_F(CookiesDetailsTest, CreateForTreeIndexedDB) {
+ scoped_nsobject<CocoaCookieDetails> details;
+
+ std::string protocol("http");
+ std::string host("moose.org");
+ unsigned short port = 80;
+ std::string database_identifier("id");
+ std::string origin("moose.org");
+ FilePath file_path(FILE_PATH_LITERAL("/"));
+ int64 size = 1234;
+ base::Time last_modified = base::Time::Now();
+ BrowsingDataIndexedDBHelper::IndexedDBInfo info(protocol,
+ host,
+ port,
+ database_identifier,
+ origin,
+ file_path,
+ size,
+ last_modified);
+
+ details.reset([[CocoaCookieDetails alloc] initWithIndexedDBInfo:&info]);
+
+ EXPECT_EQ([details.get() type], kCocoaCookieDetailsTypeTreeIndexedDB);
+ EXPECT_NSEQ(@"moose.org", [details.get() domain]);
+ EXPECT_NSEQ(@"1234 B", [details.get() fileSize]);
+ EXPECT_NSNE(@"", [details.get() lastModified]);
+
+ EXPECT_TRUE([details.get() shouldHideCookieDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowLocalStorageTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowDatabaseTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowAppCacheTreeDetailsView]);
+ EXPECT_TRUE([details.get() shouldShowIndexedDBTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowLocalStoragePromptDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowDatabasePromptDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowAppCachePromptDetailsView]);
+}
+
+TEST_F(CookiesDetailsTest, CreateForPromptDatabase) {
+ scoped_nsobject<CocoaCookieDetails> details;
+ std::string domain("chromium.org");
+ string16 name(base::SysNSStringToUTF16(@"wicked_name"));
+ string16 desc(base::SysNSStringToUTF16(@"desc"));
+ details.reset([[CocoaCookieDetails alloc] initWithDatabase:domain
+ databaseName:name
+ databaseDescription:desc
+ fileSize:94]);
+
+ EXPECT_EQ([details.get() type], kCocoaCookieDetailsTypePromptDatabase);
+ EXPECT_NSEQ(@"chromium.org", [details.get() domain]);
+ EXPECT_NSEQ(@"wicked_name", [details.get() name]);
+ EXPECT_NSEQ(@"desc", [details.get() databaseDescription]);
+ EXPECT_NSEQ(@"94 B", [details.get() fileSize]);
+
+ EXPECT_TRUE([details.get() shouldHideCookieDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowLocalStorageTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowDatabaseTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowAppCacheTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowIndexedDBTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowLocalStoragePromptDetailsView]);
+ EXPECT_TRUE([details.get() shouldShowDatabasePromptDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowAppCachePromptDetailsView]);
+}
+
+TEST_F(CookiesDetailsTest, CreateForPromptLocalStorage) {
+ scoped_nsobject<CocoaCookieDetails> details;
+ std::string domain("chromium.org");
+ string16 key(base::SysNSStringToUTF16(@"testKey"));
+ string16 value(base::SysNSStringToUTF16(@"testValue"));
+ details.reset([[CocoaCookieDetails alloc] initWithLocalStorage:domain
+ key:key
+ value:value]);
+
+ EXPECT_EQ([details.get() type], kCocoaCookieDetailsTypePromptLocalStorage);
+ EXPECT_NSEQ(@"chromium.org", [details.get() domain]);
+ EXPECT_NSEQ(@"testKey", [details.get() localStorageKey]);
+ EXPECT_NSEQ(@"testValue", [details.get() localStorageValue]);
+
+ EXPECT_TRUE([details.get() shouldHideCookieDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowLocalStorageTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowDatabaseTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowAppCacheTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowIndexedDBTreeDetailsView]);
+ EXPECT_TRUE([details.get() shouldShowLocalStoragePromptDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowDatabasePromptDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowAppCachePromptDetailsView]);
+}
+
+TEST_F(CookiesDetailsTest, CreateForPromptAppCache) {
+ scoped_nsobject<CocoaCookieDetails> details;
+ std::string manifestURL("http://html5demos.com/html5demo.manifest");
+ details.reset([[CocoaCookieDetails alloc]
+ initWithAppCacheManifestURL:manifestURL.c_str()]);
+
+ EXPECT_EQ([details.get() type], kCocoaCookieDetailsTypePromptAppCache);
+ EXPECT_NSEQ(@"http://html5demos.com/html5demo.manifest",
+ [details.get() manifestURL]);
+
+ EXPECT_TRUE([details.get() shouldHideCookieDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowLocalStorageTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowDatabaseTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowAppCacheTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowIndexedDBTreeDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowLocalStoragePromptDetailsView]);
+ EXPECT_FALSE([details.get() shouldShowDatabasePromptDetailsView]);
+ EXPECT_TRUE([details.get() shouldShowAppCachePromptDetailsView]);
+}
+
+}
diff --git a/chrome/browser/ui/cocoa/cookie_details_view_controller.h b/chrome/browser/ui/cocoa/cookie_details_view_controller.h
new file mode 100644
index 0000000..cad42f4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/cookie_details_view_controller.h
@@ -0,0 +1,56 @@
+// 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/cocoa_protocols_mac.h"
+#include "net/base/cookie_monster.h"
+
+@class CocoaCookieTreeNode;
+@class GTMUILocalizerAndLayoutTweaker;
+
+// Controller for the view that displays the details of a cookie,
+// used both in the cookie prompt dialog as well as the
+// show cookies preference sheet of content settings preferences.
+@interface CookieDetailsViewController : NSViewController {
+ @private
+ // Allows direct access to the object controller for
+ // the displayed cookie information.
+ IBOutlet NSObjectController* objectController_;
+
+ // This explicit reference to the layout tweaker is
+ // required because it's necessary to reformat the view when
+ // the content object changes, since the content object may
+ // alter the widths of some of the fields displayed in the view.
+ IBOutlet GTMUILocalizerAndLayoutTweaker* tweaker_;
+}
+
+@property (nonatomic, readonly) BOOL hasExpiration;
+
+- (id)init;
+
+// Configures the cookie detail view that is managed by the controller
+// to display the information about a single cookie, the information
+// for which is explicitly passed in the parameter |content|.
+- (void)setContentObject:(id)content;
+
+// Adjust the size of the view to exactly fix the information text fields
+// that are visible inside it.
+- (void)shrinkViewToFit;
+
+// Called by the cookie tree dialog to establish a binding between
+// the the detail view's object controller and the tree controller.
+// This binding allows the cookie tree to use the detail view unmodified.
+- (void)configureBindingsForTreeController:(NSTreeController*)controller;
+
+// Action sent by the expiration date popup when the user
+// selects the menu item "When I close my browser".
+- (IBAction)setCookieDoesntHaveExplicitExpiration:(id)sender;
+
+// Action sent by the expiration date popup when the user
+// selects the menu item with an explicit date/time of expiration.
+- (IBAction)setCookieHasExplicitExpiration:(id)sender;
+
+@end
+
diff --git a/chrome/browser/ui/cocoa/cookie_details_view_controller.mm b/chrome/browser/ui/cocoa/cookie_details_view_controller.mm
new file mode 100644
index 0000000..9f47a54
--- /dev/null
+++ b/chrome/browser/ui/cocoa/cookie_details_view_controller.mm
@@ -0,0 +1,110 @@
+// 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/ui/cocoa/cookie_details_view_controller.h"
+
+#include "app/l10n_util_mac.h"
+#include "app/resource_bundle.h"
+#import "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#import "chrome/browser/ui/cocoa/cookie_tree_node.h"
+#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+namespace {
+static const int kExtraMarginBelowWhenExpirationEditable = 5;
+}
+
+#pragma mark View Controller
+
+@implementation CookieDetailsViewController
+@dynamic hasExpiration;
+
+- (id)init {
+ return [super initWithNibName:@"CookieDetailsView"
+ bundle:mac_util::MainAppBundle()];
+}
+
+- (void)awakeFromNib {
+ DCHECK(objectController_);
+}
+
+// Finds and returns the y offset of the lowest-most non-hidden
+// text field in the view. This is used to shrink the view
+// appropriately so that it just fits its visible content.
+- (void)getLowestLabelVerticalPosition:(NSView*)view
+ lowestLabelPosition:(float&)lowestLabelPosition {
+ if (![view isHidden]) {
+ if ([view isKindOfClass:[NSTextField class]]) {
+ NSRect frame = [view frame];
+ if (frame.origin.y < lowestLabelPosition) {
+ lowestLabelPosition = frame.origin.y;
+ }
+ }
+ for (NSView* subview in [view subviews]) {
+ [self getLowestLabelVerticalPosition:subview
+ lowestLabelPosition:lowestLabelPosition];
+ }
+ }
+}
+
+- (void)setContentObject:(id)content {
+ // Make sure the view is loaded before we set the content object,
+ // otherwise, the KVO notifications to update the content don't
+ // reach the view and all of the detail values are default
+ // strings.
+ NSView* view = [self view];
+
+ [objectController_ setValue:content forKey:@"content"];
+
+ // View needs to be re-tweaked after setting the content object,
+ // since the expiration date may have changed, changing the
+ // size of the expiration popup.
+ [tweaker_ tweakUI:view];
+}
+
+- (void)shrinkViewToFit {
+ // Adjust the information pane to be exactly the right size
+ // to hold the visible text information fields.
+ NSView* view = [self view];
+ NSRect frame = [view frame];
+ float lowestLabelPosition = frame.origin.y + frame.size.height;
+ [self getLowestLabelVerticalPosition:view
+ lowestLabelPosition:lowestLabelPosition];
+ float verticalDelta = lowestLabelPosition - frame.origin.y;
+
+ // Popup menu for the expiration is taller than the plain
+ // text, give it some more room.
+ if ([[[objectController_ content] details] canEditExpiration]) {
+ verticalDelta -= kExtraMarginBelowWhenExpirationEditable;
+ }
+
+ frame.origin.y += verticalDelta;
+ frame.size.height -= verticalDelta;
+ [[self view] setFrame:frame];
+}
+
+- (void)configureBindingsForTreeController:(NSTreeController*)treeController {
+ // There seems to be a bug in the binding logic that it's not possible
+ // to bind to the selection of the tree controller, the bind seems to
+ // require an additional path segment in the key, thus the use of
+ // selection.self rather than just selection below.
+ [objectController_ bind:@"contentObject"
+ toObject:treeController
+ withKeyPath:@"selection.self"
+ options:nil];
+}
+
+- (IBAction)setCookieDoesntHaveExplicitExpiration:(id)sender {
+ [[[objectController_ content] details] setHasExpiration:NO];
+}
+
+- (IBAction)setCookieHasExplicitExpiration:(id)sender {
+ [[[objectController_ content] details] setHasExpiration:YES];
+}
+
+- (BOOL)hasExpiration {
+ return [[[objectController_ content] details] hasExpiration];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/cookie_details_view_controller_unittest.mm b/chrome/browser/ui/cocoa/cookie_details_view_controller_unittest.mm
new file mode 100644
index 0000000..4a7e5da
--- /dev/null
+++ b/chrome/browser/ui/cocoa/cookie_details_view_controller_unittest.mm
@@ -0,0 +1,88 @@
+// 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/sys_string_conversions.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/cookie_details.h"
+#include "chrome/browser/ui/cocoa/cookie_details_view_controller.h"
+
+namespace {
+
+class CookieDetailsViewControllerTest : public CocoaTest {
+};
+
+static CocoaCookieDetails* CreateTestCookieDetails(BOOL canEditExpiration) {
+ GURL url("http://chromium.org");
+ std::string cookieLine(
+ "PHPSESSID=0123456789abcdef0123456789abcdef; path=/");
+ net::CookieMonster::ParsedCookie pc(cookieLine);
+ net::CookieMonster::CanonicalCookie cookie(url, pc);
+ NSString* origin = base::SysUTF8ToNSString("http://chromium.org");
+ CocoaCookieDetails* details = [CocoaCookieDetails alloc];
+ [details initWithCookie:&cookie
+ origin:origin
+ canEditExpiration:canEditExpiration];
+ return [details autorelease];
+}
+
+static CookiePromptContentDetailsAdapter* CreateCookieTestContent(
+ BOOL canEditExpiration) {
+ CocoaCookieDetails* details = CreateTestCookieDetails(canEditExpiration);
+ return [[[CookiePromptContentDetailsAdapter alloc] initWithDetails:details]
+ autorelease];
+}
+
+static CocoaCookieDetails* CreateTestDatabaseDetails() {
+ std::string domain("chromium.org");
+ string16 name(base::SysNSStringToUTF16(@"wicked_name"));
+ string16 desc(base::SysNSStringToUTF16(@"wicked_desc"));
+ CocoaCookieDetails* details = [CocoaCookieDetails alloc];
+ [details initWithDatabase:domain
+ databaseName:name
+ databaseDescription:desc
+ fileSize:2222];
+ return [details autorelease];
+}
+
+static CookiePromptContentDetailsAdapter* CreateDatabaseTestContent() {
+ CocoaCookieDetails* details = CreateTestDatabaseDetails();
+ return [[[CookiePromptContentDetailsAdapter alloc] initWithDetails:details]
+ autorelease];
+}
+
+TEST_F(CookieDetailsViewControllerTest, Create) {
+ scoped_nsobject<CookieDetailsViewController> detailsViewController(
+ [[CookieDetailsViewController alloc] init]);
+}
+
+TEST_F(CookieDetailsViewControllerTest, ShrinkToFit) {
+ scoped_nsobject<CookieDetailsViewController> detailsViewController(
+ [[CookieDetailsViewController alloc] init]);
+ scoped_nsobject<CookiePromptContentDetailsAdapter> adapter(
+ [CreateDatabaseTestContent() retain]);
+ [detailsViewController.get() setContentObject:adapter.get()];
+ NSRect beforeFrame = [[detailsViewController.get() view] frame];
+ [detailsViewController.get() shrinkViewToFit];
+ NSRect afterFrame = [[detailsViewController.get() view] frame];
+
+ EXPECT_TRUE(afterFrame.size.height < beforeFrame.size.width);
+}
+
+TEST_F(CookieDetailsViewControllerTest, ExpirationEditability) {
+ scoped_nsobject<CookieDetailsViewController> detailsViewController(
+ [[CookieDetailsViewController alloc] init]);
+ [detailsViewController view];
+ scoped_nsobject<CookiePromptContentDetailsAdapter> adapter(
+ [CreateCookieTestContent(YES) retain]);
+ [detailsViewController.get() setContentObject:adapter.get()];
+
+ EXPECT_FALSE([detailsViewController.get() hasExpiration]);
+ [detailsViewController.get() setCookieHasExplicitExpiration:adapter.get()];
+ EXPECT_TRUE([detailsViewController.get() hasExpiration]);
+ [detailsViewController.get()
+ setCookieDoesntHaveExplicitExpiration:adapter.get()];
+ EXPECT_FALSE([detailsViewController.get() hasExpiration]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/cookie_tree_node.h b/chrome/browser/ui/cocoa/cookie_tree_node.h
new file mode 100644
index 0000000..ec1b2d2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/cookie_tree_node.h
@@ -0,0 +1,37 @@
+// 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"
+#include "chrome/browser/cookies_tree_model.h"
+#include "chrome/browser/ui/cocoa/cookie_details.h"
+
+@interface CocoaCookieTreeNode : NSObject {
+ scoped_nsobject<NSString> title_;
+ scoped_nsobject<NSMutableArray> children_;
+ scoped_nsobject<CocoaCookieDetails> details_;
+ CookieTreeNode* treeNode_; // weak
+}
+
+// Designated initializer.
+- (id)initWithNode:(CookieTreeNode*)node;
+
+// Re-sets all the members of the node based on |treeNode_|.
+- (void)rebuild;
+
+// Common getters..
+- (NSString*)title;
+- (CocoaCookieDetailsType)nodeType;
+- (TreeModelNode*)treeNode;
+
+// |-mutableChildren| exists so that the CookiesTreeModelObserverBridge can
+// operate on the children. Note that this lazily creates children.
+- (NSMutableArray*)mutableChildren;
+- (NSArray*)children;
+- (BOOL)isLeaf;
+
+- (CocoaCookieDetails*)details;
+
+@end
diff --git a/chrome/browser/ui/cocoa/cookie_tree_node.mm b/chrome/browser/ui/cocoa/cookie_tree_node.mm
new file mode 100644
index 0000000..fa4da84
--- /dev/null
+++ b/chrome/browser/ui/cocoa/cookie_tree_node.mm
@@ -0,0 +1,73 @@
+// 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/ui/cocoa/cookie_tree_node.h"
+
+#include "base/sys_string_conversions.h"
+
+@implementation CocoaCookieTreeNode
+
+- (id)initWithNode:(CookieTreeNode*)node {
+ if ((self = [super init])) {
+ DCHECK(node);
+ treeNode_ = node;
+ [self rebuild];
+ }
+ return self;
+}
+
+- (void)rebuild {
+ title_.reset([base::SysUTF16ToNSString(treeNode_->GetTitle()) retain]);
+ children_.reset();
+ // The tree node assumes ownership of the cookie details object
+ details_.reset([[CocoaCookieDetails createFromCookieTreeNode:(treeNode_)]
+ retain]);
+}
+
+- (NSString*)title {
+ return title_.get();
+}
+
+- (CocoaCookieDetailsType)nodeType {
+ return [details_.get() type];
+}
+
+- (TreeModelNode*)treeNode {
+ return treeNode_;
+}
+
+- (NSMutableArray*)mutableChildren {
+ if (!children_.get()) {
+ const int childCount = treeNode_->GetChildCount();
+ children_.reset([[NSMutableArray alloc] initWithCapacity:childCount]);
+ for (int i = 0; i < childCount; ++i) {
+ CookieTreeNode* child = treeNode_->GetChild(i);
+ scoped_nsobject<CocoaCookieTreeNode> childNode(
+ [[CocoaCookieTreeNode alloc] initWithNode:child]);
+ [children_ addObject:childNode.get()];
+ }
+ }
+ return children_.get();
+}
+
+- (NSArray*)children {
+ return [self mutableChildren];
+}
+
+- (BOOL)isLeaf {
+ return [self nodeType] != kCocoaCookieDetailsTypeFolder;
+};
+
+- (NSString*)description {
+ NSString* format =
+ @"<CocoaCookieTreeNode @ %p (title=%@, nodeType=%d, childCount=%u)";
+ return [NSString stringWithFormat:format, self, [self title],
+ [self nodeType], [[self children] count]];
+}
+
+- (CocoaCookieDetails*)details {
+ return details_;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/cookies_window_controller.h b/chrome/browser/ui/cocoa/cookies_window_controller.h
new file mode 100644
index 0000000..0dd8004
--- /dev/null
+++ b/chrome/browser/ui/cocoa/cookies_window_controller.h
@@ -0,0 +1,146 @@
+// 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/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/cookies_tree_model.h"
+#import "chrome/browser/ui/cocoa/cookie_tree_node.h"
+#include "net/base/cookie_monster.h"
+
+@class CookiesWindowController;
+@class CookieDetailsViewController;
+class Profile;
+class TreeModel;
+class TreeModelNode;
+
+namespace {
+class CookiesWindowControllerTest;
+}
+
+// Thin bridge to the window controller that performs model update actions
+// directly on the treeController_.
+class CookiesTreeModelObserverBridge : public CookiesTreeModel::Observer {
+ public:
+ explicit CookiesTreeModelObserverBridge(CookiesWindowController* controller);
+
+ // Begin TreeModelObserver implementation.
+ virtual void TreeNodesAdded(TreeModel* model,
+ TreeModelNode* parent,
+ int start,
+ int count);
+ virtual void TreeNodesRemoved(TreeModel* model,
+ TreeModelNode* parent,
+ int start,
+ int count);
+ virtual void TreeNodeChanged(TreeModel* model, TreeModelNode* node);
+ // End TreeModelObserver implementation.
+
+ virtual void TreeModelBeginBatch(CookiesTreeModel* model);
+ virtual void TreeModelEndBatch(CookiesTreeModel* model);
+
+ // Invalidates the Cocoa model. This is used to tear down the Cocoa model
+ // when we're about to entirely rebuild it.
+ void InvalidateCocoaModel();
+
+ private:
+ friend class ::CookiesWindowControllerTest;
+
+ // Creates a CocoaCookieTreeNode from a platform-independent one.
+ // Return value is autoreleased. This creates child nodes recusively.
+ CocoaCookieTreeNode* CocoaNodeFromTreeNode(TreeModelNode* node);
+
+ // Finds the Cocoa model node based on a platform-independent one. This is
+ // done by comparing the treeNode pointers. |start| is the node to start
+ // searching at. If |start| is nil, the root is used.
+ CocoaCookieTreeNode* FindCocoaNode(TreeModelNode* node,
+ CocoaCookieTreeNode* start);
+
+ // Returns whether or not the Cocoa tree model is built.
+ bool HasCocoaModel();
+
+ CookiesWindowController* window_controller_; // weak, owns us.
+
+ // If this is true, then the Model has informed us that it is batching
+ // updates. Rather than updating the Cocoa side of the model, we ignore those
+ // small changes and rebuild once at the end.
+ bool batch_update_;
+};
+
+// Controller for the cookies manager. This class stores an internal copy of
+// the CookiesTreeModel but with Cocoa-converted values (NSStrings and NSImages
+// instead of std::strings and SkBitmaps). Doing this allows us to use bindings
+// for the interface. Changes are pushed to this internal model via a very thin
+// bridge (see above).
+@interface CookiesWindowController : NSWindowController
+ <NSOutlineViewDelegate,
+ NSWindowDelegate> {
+ @private
+ // Platform-independent model and C++/Obj-C bridge components.
+ scoped_ptr<CookiesTreeModel> treeModel_;
+ scoped_ptr<CookiesTreeModelObserverBridge> modelObserver_;
+
+ // Cached array of icons.
+ scoped_nsobject<NSMutableArray> icons_;
+
+ // Our Cocoa copy of the model.
+ scoped_nsobject<CocoaCookieTreeNode> cocoaTreeModel_;
+
+ // A flag indicating whether or not the "Remove" button should be enabled.
+ BOOL removeButtonEnabled_;
+
+ IBOutlet NSTreeController* treeController_;
+ IBOutlet NSOutlineView* outlineView_;
+ IBOutlet NSSearchField* searchField_;
+ IBOutlet NSView* cookieDetailsViewPlaceholder_;
+ IBOutlet NSButton* removeButton_;
+
+ scoped_nsobject<CookieDetailsViewController> detailsViewController_;
+ Profile* profile_; // weak
+ BrowsingDataDatabaseHelper* databaseHelper_; // weak
+ BrowsingDataLocalStorageHelper* storageHelper_; // weak
+ BrowsingDataAppCacheHelper* appcacheHelper_; // weak
+ BrowsingDataIndexedDBHelper* indexedDBHelper_; // weak
+}
+@property (assign, nonatomic) BOOL removeButtonEnabled;
+@property (readonly, nonatomic) NSTreeController* treeController;
+
+// Designated initializer. Profile cannot be NULL.
+- (id)initWithProfile:(Profile*)profile
+ databaseHelper:(BrowsingDataDatabaseHelper*)databaseHelper
+ storageHelper:(BrowsingDataLocalStorageHelper*)storageHelper
+ appcacheHelper:(BrowsingDataAppCacheHelper*)appcacheHelper
+ indexedDBHelper:(BrowsingDataIndexedDBHelper*)indexedDBHelper;
+
+// Shows the cookies window as a modal sheet attached to |window|.
+- (void)attachSheetTo:(NSWindow*)window;
+
+// Updates the filter from the search field.
+- (IBAction)updateFilter:(id)sender;
+
+// Delete cookie actions.
+- (IBAction)deleteCookie:(id)sender;
+- (IBAction)deleteAllCookies:(id)sender;
+
+// Closes the sheet and ends the modal loop. This will also cleanup the memory.
+- (IBAction)closeSheet:(id)sender;
+
+// Returns the cocoaTreeModel_.
+- (CocoaCookieTreeNode*)cocoaTreeModel;
+- (void)setCocoaTreeModel:(CocoaCookieTreeNode*)model;
+
+// Returns the treeModel_.
+- (CookiesTreeModel*)treeModel;
+
+@end
+
+@interface CookiesWindowController (UnitTesting)
+- (void)deleteNodeAtIndexPath:(NSIndexPath*)path;
+- (void)clearBrowsingDataNotification:(NSNotification*)notif;
+- (CookiesTreeModelObserverBridge*)modelObserver;
+- (NSArray*)icons;
+- (void)loadTreeModelFromProfile;
+@end
diff --git a/chrome/browser/ui/cocoa/cookies_window_controller.mm b/chrome/browser/ui/cocoa/cookies_window_controller.mm
new file mode 100644
index 0000000..ac95301
--- /dev/null
+++ b/chrome/browser/ui/cocoa/cookies_window_controller.mm
@@ -0,0 +1,448 @@
+// 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/ui/cocoa/cookies_window_controller.h"
+
+#include <queue>
+#include <vector>
+
+#include "app/l10n_util_mac.h"
+#include "app/resource_bundle.h"
+#import "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/browsing_data_remover.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/ui/cocoa/clear_browsing_data_controller.h"
+#include "chrome/browser/ui/cocoa/cookie_details_view_controller.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+#include "third_party/apple/ImageAndTextCell.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+// Key path used for notifying KVO.
+static NSString* const kCocoaTreeModel = @"cocoaTreeModel";
+
+CookiesTreeModelObserverBridge::CookiesTreeModelObserverBridge(
+ CookiesWindowController* controller)
+ : window_controller_(controller),
+ batch_update_(false) {
+}
+
+// Notification that nodes were added to the specified parent.
+void CookiesTreeModelObserverBridge::TreeNodesAdded(TreeModel* model,
+ TreeModelNode* parent,
+ int start,
+ int count) {
+ // We're in for a major rebuild. Ignore this request.
+ if (batch_update_ || !HasCocoaModel())
+ return;
+
+ CocoaCookieTreeNode* cocoa_parent = FindCocoaNode(parent, nil);
+ NSMutableArray* cocoa_children = [cocoa_parent mutableChildren];
+
+ [window_controller_ willChangeValueForKey:kCocoaTreeModel];
+ CookieTreeNode* cookie_parent = static_cast<CookieTreeNode*>(parent);
+ for (int i = 0; i < count; ++i) {
+ CookieTreeNode* cookie_child = cookie_parent->GetChild(start + i);
+ CocoaCookieTreeNode* new_child = CocoaNodeFromTreeNode(cookie_child);
+ [cocoa_children addObject:new_child];
+ }
+ [window_controller_ didChangeValueForKey:kCocoaTreeModel];
+}
+
+// Notification that nodes were removed from the specified parent.
+void CookiesTreeModelObserverBridge::TreeNodesRemoved(TreeModel* model,
+ TreeModelNode* parent,
+ int start,
+ int count) {
+ // We're in for a major rebuild. Ignore this request.
+ if (batch_update_ || !HasCocoaModel())
+ return;
+
+ CocoaCookieTreeNode* cocoa_parent = FindCocoaNode(parent, nil);
+ [window_controller_ willChangeValueForKey:kCocoaTreeModel];
+ NSMutableArray* cocoa_children = [cocoa_parent mutableChildren];
+ for (int i = start + count - 1; i >= start; --i) {
+ [cocoa_children removeObjectAtIndex:i];
+ }
+ [window_controller_ didChangeValueForKey:kCocoaTreeModel];
+}
+
+// Notification that the contents of a node has changed.
+void CookiesTreeModelObserverBridge::TreeNodeChanged(TreeModel* model,
+ TreeModelNode* node) {
+ // If we don't have a Cocoa model, only let the root node change.
+ if (batch_update_ || (!HasCocoaModel() && model->GetRoot() != node))
+ return;
+
+ if (HasCocoaModel()) {
+ // We still have a Cocoa model, so just rebuild the node.
+ [window_controller_ willChangeValueForKey:kCocoaTreeModel];
+ CocoaCookieTreeNode* changed_node = FindCocoaNode(node, nil);
+ [changed_node rebuild];
+ [window_controller_ didChangeValueForKey:kCocoaTreeModel];
+ } else {
+ // Full rebuild.
+ [window_controller_ setCocoaTreeModel:CocoaNodeFromTreeNode(node)];
+ }
+}
+
+void CookiesTreeModelObserverBridge::TreeModelBeginBatch(
+ CookiesTreeModel* model) {
+ batch_update_ = true;
+}
+
+void CookiesTreeModelObserverBridge::TreeModelEndBatch(
+ CookiesTreeModel* model) {
+ DCHECK(batch_update_);
+ CocoaCookieTreeNode* root = CocoaNodeFromTreeNode(model->GetRoot());
+ [window_controller_ setCocoaTreeModel:root];
+ batch_update_ = false;
+}
+
+void CookiesTreeModelObserverBridge::InvalidateCocoaModel() {
+ [[[window_controller_ cocoaTreeModel] mutableChildren] removeAllObjects];
+}
+
+CocoaCookieTreeNode* CookiesTreeModelObserverBridge::CocoaNodeFromTreeNode(
+ TreeModelNode* node) {
+ CookieTreeNode* cookie_node = static_cast<CookieTreeNode*>(node);
+ return [[[CocoaCookieTreeNode alloc] initWithNode:cookie_node] autorelease];
+}
+
+// Does breadth-first search on the tree to find |node|. This method is most
+// commonly used to find origin/folder nodes, which are at the first level off
+// the root (hence breadth-first search).
+CocoaCookieTreeNode* CookiesTreeModelObserverBridge::FindCocoaNode(
+ TreeModelNode* target, CocoaCookieTreeNode* start) {
+ if (!start) {
+ start = [window_controller_ cocoaTreeModel];
+ }
+ if ([start treeNode] == target) {
+ return start;
+ }
+
+ // Enqueue the root node of the search (sub-)tree.
+ std::queue<CocoaCookieTreeNode*> horizon;
+ horizon.push(start);
+
+ // Loop until we've looked at every node or we found the target.
+ while (!horizon.empty()) {
+ // Dequeue the item at the front.
+ CocoaCookieTreeNode* node = horizon.front();
+ horizon.pop();
+
+ // If this is the droid we're looking for, report it.
+ if ([node treeNode] == target)
+ return node;
+
+ // "Move along, move along." by adding all child nodes to the queue.
+ if (![node isLeaf]) {
+ NSArray* children = [node children];
+ for (CocoaCookieTreeNode* child in children) {
+ horizon.push(child);
+ }
+ }
+ }
+
+ return nil; // We couldn't find the node.
+}
+
+// Returns whether or not the Cocoa tree model is built.
+bool CookiesTreeModelObserverBridge::HasCocoaModel() {
+ return ([[[window_controller_ cocoaTreeModel] children] count] > 0U);
+}
+
+#pragma mark Window Controller
+
+@implementation CookiesWindowController
+
+@synthesize removeButtonEnabled = removeButtonEnabled_;
+@synthesize treeController = treeController_;
+
+- (id)initWithProfile:(Profile*)profile
+ databaseHelper:(BrowsingDataDatabaseHelper*)databaseHelper
+ storageHelper:(BrowsingDataLocalStorageHelper*)storageHelper
+ appcacheHelper:(BrowsingDataAppCacheHelper*)appcacheHelper
+ indexedDBHelper:(BrowsingDataIndexedDBHelper*)indexedDBHelper {
+ DCHECK(profile);
+ NSString* nibpath = [mac_util::MainAppBundle() pathForResource:@"Cookies"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ profile_ = profile;
+ databaseHelper_ = databaseHelper;
+ storageHelper_ = storageHelper;
+ appcacheHelper_ = appcacheHelper;
+ indexedDBHelper_ = indexedDBHelper;
+
+ [self loadTreeModelFromProfile];
+
+ // Register for Clear Browsing Data controller so we update appropriately.
+ ClearBrowsingDataController* clearingController =
+ [ClearBrowsingDataController controllerForProfile:profile_];
+ if (clearingController) {
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
+ selector:@selector(clearBrowsingDataNotification:)
+ name:kClearBrowsingDataControllerDidDelete
+ object:clearingController];
+ }
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+- (void)awakeFromNib {
+ DCHECK([self window]);
+ DCHECK_EQ(self, [[self window] delegate]);
+
+ detailsViewController_.reset([[CookieDetailsViewController alloc] init]);
+
+ NSView* detailView = [detailsViewController_.get() view];
+ NSRect viewFrameRect = [cookieDetailsViewPlaceholder_ frame];
+ [[detailsViewController_.get() view] setFrame:viewFrameRect];
+ [[cookieDetailsViewPlaceholder_ superview]
+ replaceSubview:cookieDetailsViewPlaceholder_
+ with:detailView];
+
+ [detailsViewController_ configureBindingsForTreeController:treeController_];
+}
+
+- (void)windowWillClose:(NSNotification*)notif {
+ [searchField_ setTarget:nil];
+ [outlineView_ setDelegate:nil];
+ [self autorelease];
+}
+
+- (void)attachSheetTo:(NSWindow*)window {
+ [NSApp beginSheet:[self window]
+ modalForWindow:window
+ modalDelegate:self
+ didEndSelector:@selector(sheetEndSheet:returnCode:contextInfo:)
+ contextInfo:nil];
+}
+
+- (void)sheetEndSheet:(NSWindow*)sheet
+ returnCode:(NSInteger)returnCode
+ contextInfo:(void*)context {
+ [sheet close];
+ [sheet orderOut:self];
+}
+
+- (IBAction)updateFilter:(id)sender {
+ DCHECK([sender isKindOfClass:[NSSearchField class]]);
+ NSString* string = [sender stringValue];
+ // Invalidate the model here because all the nodes are going to be removed
+ // in UpdateSearchResults(). This could lead to there temporarily being
+ // invalid pointers in the Cocoa model.
+ modelObserver_->InvalidateCocoaModel();
+ treeModel_->UpdateSearchResults(base::SysNSStringToWide(string));
+}
+
+- (IBAction)deleteCookie:(id)sender {
+ DCHECK_EQ(1U, [[treeController_ selectedObjects] count]);
+ [self deleteNodeAtIndexPath:[treeController_ selectionIndexPath]];
+}
+
+// This will delete the Cocoa model node as well as the backing model object at
+// the specified index path in the Cocoa model. If the node that was deleted
+// was the sole child of the parent node, this will be called recursively to
+// delete empty parents.
+- (void)deleteNodeAtIndexPath:(NSIndexPath*)path {
+ NSTreeNode* treeNode =
+ [[treeController_ arrangedObjects] descendantNodeAtIndexPath:path];
+ if (!treeNode)
+ return;
+
+ CocoaCookieTreeNode* node = [treeNode representedObject];
+ CookieTreeNode* cookie = static_cast<CookieTreeNode*>([node treeNode]);
+ treeModel_->DeleteCookieNode(cookie);
+ // If there is a next cookie, this will select it because items will slide
+ // up. If there is no next cookie, this is a no-op.
+ [treeController_ setSelectionIndexPath:path];
+ // If the above setting of the selection was in fact a no-op, find the next
+ // node to select.
+ if (![[treeController_ selectedObjects] count]) {
+ NSUInteger lastIndex = [path indexAtPosition:[path length] - 1];
+ if (lastIndex != 0) {
+ // If there any nodes remaining, select the node that is in the list
+ // before this one.
+ path = [path indexPathByRemovingLastIndex];
+ path = [path indexPathByAddingIndex:lastIndex - 1];
+ [treeController_ setSelectionIndexPath:path];
+ }
+ }
+}
+
+- (IBAction)deleteAllCookies:(id)sender {
+ // Preemptively delete all cookies in the Cocoa model.
+ modelObserver_->InvalidateCocoaModel();
+ treeModel_->DeleteAllStoredObjects();
+}
+
+- (IBAction)closeSheet:(id)sender {
+ [NSApp endSheet:[self window]];
+}
+
+- (void)clearBrowsingDataNotification:(NSNotification*)notif {
+ NSNumber* removeMask =
+ [[notif userInfo] objectForKey:kClearBrowsingDataControllerRemoveMask];
+ if ([removeMask intValue] & BrowsingDataRemover::REMOVE_COOKIES) {
+ [self loadTreeModelFromProfile];
+ }
+}
+
+// Override keyDown on the controller (which is the first responder) to allow
+// both backspace and delete to be captured by the Remove button.
+- (void)keyDown:(NSEvent*)theEvent {
+ NSString* keys = [theEvent characters];
+ if ([keys length]) {
+ unichar key = [keys characterAtIndex:0];
+ // The button has a key equivalent of backspace, so examine this event for
+ // forward delete.
+ if ((key == NSDeleteCharacter || key == NSDeleteFunctionKey) &&
+ [self removeButtonEnabled]) {
+ [removeButton_ performClick:self];
+ return;
+ }
+ }
+ [super keyDown:theEvent];
+}
+
+#pragma mark Getters and Setters
+
+- (CocoaCookieTreeNode*)cocoaTreeModel {
+ return cocoaTreeModel_.get();
+}
+- (void)setCocoaTreeModel:(CocoaCookieTreeNode*)model {
+ cocoaTreeModel_.reset([model retain]);
+}
+
+- (CookiesTreeModel*)treeModel {
+ return treeModel_.get();
+}
+
+#pragma mark Outline View Delegate
+
+- (void)outlineView:(NSOutlineView*)outlineView
+ willDisplayCell:(id)cell
+ forTableColumn:(NSTableColumn*)tableColumn
+ item:(id)item {
+ CocoaCookieTreeNode* node = [item representedObject];
+ int index = treeModel_->GetIconIndex([node treeNode]);
+ NSImage* icon = nil;
+ if (index >= 0)
+ icon = [icons_ objectAtIndex:index];
+ else
+ icon = [icons_ lastObject];
+ [(ImageAndTextCell*)cell setImage:icon];
+}
+
+- (void)outlineViewItemDidExpand:(NSNotification*)notif {
+ NSTreeNode* item = [[notif userInfo] objectForKey:@"NSObject"];
+ CocoaCookieTreeNode* node = [item representedObject];
+ NSArray* children = [node children];
+ if ([children count] == 1U) {
+ // The node that will expand has one child. Do the user a favor and expand
+ // that node (saving her a click) if it is non-leaf.
+ CocoaCookieTreeNode* child = [children lastObject];
+ if (![child isLeaf]) {
+ NSOutlineView* outlineView = [notif object];
+ // Tell the OutlineView to expand the NSTreeNode, not the model object.
+ children = [item childNodes];
+ DCHECK_EQ([children count], 1U);
+ [outlineView expandItem:[children lastObject]];
+ // Select the first node in that child set.
+ NSTreeNode* folderChild = [children lastObject];
+ if ([[folderChild childNodes] count] > 0) {
+ NSTreeNode* firstCookieChild =
+ [[folderChild childNodes] objectAtIndex:0];
+ [treeController_ setSelectionIndexPath:[firstCookieChild indexPath]];
+ }
+ }
+ }
+}
+
+- (void)outlineViewSelectionDidChange:(NSNotification*)notif {
+ // Multi-selection should be disabled in the UI, but for sanity, double-check
+ // that they can't do it here.
+ NSArray* selectedObjects = [treeController_ selectedObjects];
+ NSUInteger count = [selectedObjects count];
+ if (count != 1U) {
+ DCHECK_LT(count, 1U) << "User was able to select more than 1 cookie node!";
+ [self setRemoveButtonEnabled:NO];
+ return;
+ }
+
+ // Go through the selection's indexPath and make sure that the node that is
+ // being referenced actually exists in the Cocoa model.
+ NSIndexPath* selection = [treeController_ selectionIndexPath];
+ NSUInteger length = [selection length];
+ CocoaCookieTreeNode* node = [self cocoaTreeModel];
+ for (NSUInteger i = 0; i < length; ++i) {
+ NSUInteger childIndex = [selection indexAtPosition:i];
+ if (childIndex >= [[node children] count]) {
+ [self setRemoveButtonEnabled:NO];
+ return;
+ }
+ node = [[node children] objectAtIndex:childIndex];
+ }
+
+ // If there is a valid selection, make sure that the remove
+ // button is enabled.
+ [self setRemoveButtonEnabled:YES];
+}
+
+#pragma mark Unit Testing
+
+- (CookiesTreeModelObserverBridge*)modelObserver {
+ return modelObserver_.get();
+}
+
+- (NSArray*)icons {
+ return icons_.get();
+}
+
+// Re-initializes the |treeModel_|, creates a new observer for it, and re-
+// builds the |cocoaTreeModel_|. We use this to initialize the controller and
+// to rebuild after the user clears browsing data. Because the models get
+// clobbered, we rebuild the icon cache for safety (though they do not change).
+- (void)loadTreeModelFromProfile {
+ treeModel_.reset(new CookiesTreeModel(
+ profile_->GetRequestContext()->GetCookieStore()->GetCookieMonster(),
+ databaseHelper_,
+ storageHelper_,
+ NULL,
+ appcacheHelper_,
+ indexedDBHelper_));
+ modelObserver_.reset(new CookiesTreeModelObserverBridge(self));
+ treeModel_->AddObserver(modelObserver_.get());
+
+ // Convert the model's icons from Skia to Cocoa.
+ std::vector<SkBitmap> skiaIcons;
+ treeModel_->GetIcons(&skiaIcons);
+ icons_.reset([[NSMutableArray alloc] init]);
+ for (std::vector<SkBitmap>::iterator it = skiaIcons.begin();
+ it != skiaIcons.end(); ++it) {
+ [icons_ addObject:gfx::SkBitmapToNSImage(*it)];
+ }
+
+ // Default icon will be the last item in the array.
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ // TODO(rsesek): Rename this resource now that it's in multiple places.
+ [icons_ addObject:rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER)];
+
+ // Create the Cocoa model.
+ CookieTreeNode* root = static_cast<CookieTreeNode*>(treeModel_->GetRoot());
+ scoped_nsobject<CocoaCookieTreeNode> model(
+ [[CocoaCookieTreeNode alloc] initWithNode:root]);
+ [self setCocoaTreeModel:model.get()]; // Takes ownership.
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/cookies_window_controller_unittest.mm b/chrome/browser/ui/cocoa/cookies_window_controller_unittest.mm
new file mode 100644
index 0000000..9f3f410
--- /dev/null
+++ b/chrome/browser/ui/cocoa/cookies_window_controller_unittest.mm
@@ -0,0 +1,687 @@
+// 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 "app/l10n_util_mac.h"
+#include "app/tree_model.h"
+#import "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/browsing_data_remover.h"
+#include "chrome/browser/cookies_tree_model.h"
+#include "chrome/browser/mock_browsing_data_database_helper.h"
+#include "chrome/browser/mock_browsing_data_local_storage_helper.h"
+#include "chrome/browser/mock_browsing_data_appcache_helper.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/clear_browsing_data_controller.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/cookies_window_controller.h"
+#include "chrome/common/net/url_request_context_getter.h"
+#include "chrome/test/testing_profile.h"
+#include "googleurl/src/gurl.h"
+#include "grit/generated_resources.h"
+#include "net/url_request/url_request_context.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+
+// Used to test FindCocoaNode. This only sets the title and node, without
+// initializing any other members.
+@interface FakeCocoaCookieTreeNode : CocoaCookieTreeNode {
+ TreeModelNode* testNode_;
+}
+- (id)initWithTreeNode:(TreeModelNode*)node;
+@end
+@implementation FakeCocoaCookieTreeNode
+- (id)initWithTreeNode:(TreeModelNode*)node {
+ if ((self = [super init])) {
+ testNode_ = node;
+ children_.reset([[NSMutableArray alloc] init]);
+ }
+ return self;
+}
+- (TreeModelNode*)treeNode {
+ return testNode_;
+}
+@end
+
+namespace {
+
+class CookiesWindowControllerTest : public CocoaTest {
+ public:
+
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ TestingProfile* profile = browser_helper_.profile();
+ profile->CreateRequestContext();
+ database_helper_ = new MockBrowsingDataDatabaseHelper(profile);
+ local_storage_helper_ = new MockBrowsingDataLocalStorageHelper(profile);
+ appcache_helper_ = new MockBrowsingDataAppCacheHelper(profile);
+ controller_.reset(
+ [[CookiesWindowController alloc] initWithProfile:profile
+ databaseHelper:database_helper_
+ storageHelper:local_storage_helper_
+ appcacheHelper:appcache_helper_]
+ );
+ }
+
+ virtual void TearDown() {
+ CocoaTest::TearDown();
+ }
+
+ CocoaCookieTreeNode* CocoaNodeFromTreeNode(TreeModelNode* node) {
+ return [controller_ modelObserver]->CocoaNodeFromTreeNode(node);
+ }
+
+ CocoaCookieTreeNode* FindCocoaNode(TreeModelNode* node,
+ CocoaCookieTreeNode* start) {
+ return [controller_ modelObserver]->FindCocoaNode(node, start);
+ }
+
+ protected:
+ BrowserTestHelper browser_helper_;
+ scoped_nsobject<CookiesWindowController> controller_;
+ MockBrowsingDataDatabaseHelper* database_helper_;
+ MockBrowsingDataLocalStorageHelper* local_storage_helper_;
+ MockBrowsingDataAppCacheHelper* appcache_helper_;
+};
+
+TEST_F(CookiesWindowControllerTest, Construction) {
+ std::vector<SkBitmap> skia_icons;
+ [controller_ treeModel]->GetIcons(&skia_icons);
+
+ EXPECT_EQ([[controller_ icons] count], skia_icons.size() + 1U);
+}
+
+TEST_F(CookiesWindowControllerTest, FindCocoaNodeRoot) {
+ scoped_ptr< TreeNodeWithValue<int> > search(new TreeNodeWithValue<int>(42));
+ scoped_nsobject<FakeCocoaCookieTreeNode> node(
+ [[FakeCocoaCookieTreeNode alloc] initWithTreeNode:search.get()]);
+ EXPECT_EQ(node.get(), FindCocoaNode(search.get(), node.get()));
+}
+
+TEST_F(CookiesWindowControllerTest, FindCocoaNodeImmediateChild) {
+ scoped_ptr< TreeNodeWithValue<int> > parent(new TreeNodeWithValue<int>(100));
+ scoped_ptr< TreeNodeWithValue<int> > child1(new TreeNodeWithValue<int>(10));
+ scoped_ptr< TreeNodeWithValue<int> > child2(new TreeNodeWithValue<int>(20));
+ scoped_nsobject<FakeCocoaCookieTreeNode> cocoaParent(
+ [[FakeCocoaCookieTreeNode alloc] initWithTreeNode:parent.get()]);
+ scoped_nsobject<FakeCocoaCookieTreeNode> cocoaChild1(
+ [[FakeCocoaCookieTreeNode alloc] initWithTreeNode:child1.get()]);
+ scoped_nsobject<FakeCocoaCookieTreeNode> cocoaChild2(
+ [[FakeCocoaCookieTreeNode alloc] initWithTreeNode:child2.get()]);
+ [[cocoaParent mutableChildren] addObject:cocoaChild1.get()];
+ [[cocoaParent mutableChildren] addObject:cocoaChild2.get()];
+
+ EXPECT_EQ(cocoaChild2.get(), FindCocoaNode(child2.get(), cocoaParent.get()));
+}
+
+TEST_F(CookiesWindowControllerTest, FindCocoaNodeRecursive) {
+ scoped_ptr< TreeNodeWithValue<int> > parent(new TreeNodeWithValue<int>(100));
+ scoped_ptr< TreeNodeWithValue<int> > child1(new TreeNodeWithValue<int>(10));
+ scoped_ptr< TreeNodeWithValue<int> > child2(new TreeNodeWithValue<int>(20));
+ scoped_nsobject<FakeCocoaCookieTreeNode> cocoaParent(
+ [[FakeCocoaCookieTreeNode alloc] initWithTreeNode:parent.get()]);
+ scoped_nsobject<FakeCocoaCookieTreeNode> cocoaChild1(
+ [[FakeCocoaCookieTreeNode alloc] initWithTreeNode:child1.get()]);
+ scoped_nsobject<FakeCocoaCookieTreeNode> cocoaChild2(
+ [[FakeCocoaCookieTreeNode alloc] initWithTreeNode:child2.get()]);
+ [[cocoaParent mutableChildren] addObject:cocoaChild1.get()];
+ [[cocoaChild1 mutableChildren] addObject:cocoaChild2.get()];
+
+ EXPECT_EQ(cocoaChild2.get(), FindCocoaNode(child2.get(), cocoaParent.get()));
+}
+
+TEST_F(CookiesWindowControllerTest, CocoaNodeFromTreeNodeCookie) {
+ net::CookieMonster* cm = browser_helper_.profile()->GetCookieMonster();
+ cm->SetCookie(GURL("http://foo.com"), "A=B");
+ CookiesTreeModel model(cm, database_helper_, local_storage_helper_, nil, nil);
+
+ // Root --> foo.com --> Cookies --> A. Create node for 'A'.
+ TreeModelNode* node = model.GetRoot()->GetChild(0)->GetChild(0)->GetChild(0);
+ CocoaCookieTreeNode* cookie = CocoaNodeFromTreeNode(node);
+
+ CocoaCookieDetails* details = [cookie details];
+ EXPECT_NSEQ(@"B", [details content]);
+ EXPECT_NSEQ(l10n_util::GetNSString(IDS_COOKIES_COOKIE_EXPIRES_SESSION),
+ [details expires]);
+ EXPECT_NSEQ(l10n_util::GetNSString(IDS_COOKIES_COOKIE_SENDFOR_ANY),
+ [details sendFor]);
+ EXPECT_NSEQ(@"A", [cookie title]);
+ EXPECT_NSEQ(@"A", [details name]);
+ EXPECT_NSEQ(@"/", [details path]);
+ EXPECT_EQ(0U, [[cookie children] count]);
+ EXPECT_TRUE([details created]);
+ EXPECT_TRUE([cookie isLeaf]);
+ EXPECT_EQ(node, [cookie treeNode]);
+}
+
+TEST_F(CookiesWindowControllerTest, CocoaNodeFromTreeNodeRecursive) {
+ net::CookieMonster* cm = browser_helper_.profile()->GetCookieMonster();
+ cm->SetCookie(GURL("http://foo.com"), "A=B");
+ CookiesTreeModel model(cm, database_helper_, local_storage_helper_, nil, nil);
+
+ // Root --> foo.com --> Cookies --> A. Create node for 'foo.com'.
+ CookieTreeNode* node = model.GetRoot()->GetChild(0);
+ CocoaCookieTreeNode* domain = CocoaNodeFromTreeNode(node);
+ CocoaCookieTreeNode* cookies = [[domain children] objectAtIndex:0];
+ CocoaCookieTreeNode* cookie = [[cookies children] objectAtIndex:0];
+
+ // Test domain-level node.
+ EXPECT_NSEQ(@"foo.com", [domain title]);
+
+ EXPECT_FALSE([domain isLeaf]);
+ EXPECT_EQ(1U, [[domain children] count]);
+ EXPECT_EQ(node, [domain treeNode]);
+
+ // Test "Cookies" folder node.
+ EXPECT_NSEQ(l10n_util::GetNSString(IDS_COOKIES_COOKIES), [cookies title]);
+ EXPECT_FALSE([cookies isLeaf]);
+ EXPECT_EQ(1U, [[cookies children] count]);
+ EXPECT_EQ(node->GetChild(0), [cookies treeNode]);
+
+ // Test cookie node. This is the same as CocoaNodeFromTreeNodeCookie.
+ CocoaCookieDetails* details = [cookie details];
+ EXPECT_NSEQ(@"B", [details content]);
+ EXPECT_NSEQ(l10n_util::GetNSString(IDS_COOKIES_COOKIE_EXPIRES_SESSION),
+ [details expires]);
+ EXPECT_NSEQ(l10n_util::GetNSString(IDS_COOKIES_COOKIE_SENDFOR_ANY),
+ [details sendFor]);
+ EXPECT_NSEQ(@"A", [cookie title]);
+ EXPECT_NSEQ(@"A", [details name]);
+ EXPECT_NSEQ(@"/", [details path]);
+ EXPECT_NSEQ(@"foo.com", [details domain]);
+ EXPECT_EQ(0U, [[cookie children] count]);
+ EXPECT_TRUE([details created]);
+ EXPECT_TRUE([cookie isLeaf]);
+ EXPECT_EQ(node->GetChild(0)->GetChild(0), [cookie treeNode]);
+}
+
+TEST_F(CookiesWindowControllerTest, TreeNodesAdded) {
+ const GURL url = GURL("http://foo.com");
+ TestingProfile* profile = browser_helper_.profile();
+ net::CookieMonster* cm = profile->GetCookieMonster();
+ cm->SetCookie(url, "A=B");
+
+ controller_.reset(
+ [[CookiesWindowController alloc] initWithProfile:profile
+ databaseHelper:database_helper_
+ storageHelper:local_storage_helper_
+ appcacheHelper:appcache_helper_]);
+
+ // Root --> foo.com --> Cookies.
+ NSMutableArray* cocoa_children =
+ [[[[[[controller_ cocoaTreeModel] children] objectAtIndex:0]
+ children] objectAtIndex:0] mutableChildren];
+ EXPECT_EQ(1U, [cocoa_children count]);
+
+ // Create some cookies.
+ cm->SetCookie(url, "C=D");
+ cm->SetCookie(url, "E=F");
+
+ net::CookieMonster::CookieList list = cm->GetAllCookies();
+ CookiesTreeModel* model = [controller_ treeModel];
+ // Root --> foo.com --> Cookies.
+ CookieTreeNode* parent = model->GetRoot()->GetChild(0)->GetChild(0);
+
+ ASSERT_EQ(3U, list.size());
+
+ // Add the cookie nodes.
+ CookieTreeCookieNode* cnode = new CookieTreeCookieNode(&list[1]);
+ parent->Add(1, cnode); // |parent| takes ownership.
+ cnode = new CookieTreeCookieNode(&list[2]);
+ parent->Add(2, cnode);
+
+ // Manually notify the observer.
+ [controller_ modelObserver]->TreeNodesAdded(model, parent, 1, 2);
+
+ // Check that we have created 2 more Cocoa nodes.
+ EXPECT_EQ(3U, [cocoa_children count]);
+}
+
+TEST_F(CookiesWindowControllerTest, TreeNodesRemoved) {
+ const GURL url = GURL("http://foo.com");
+ TestingProfile* profile = browser_helper_.profile();
+ net::CookieMonster* cm = profile->GetCookieMonster();
+ cm->SetCookie(url, "A=B");
+ cm->SetCookie(url, "C=D");
+ cm->SetCookie(url, "E=F");
+
+ controller_.reset(
+ [[CookiesWindowController alloc] initWithProfile:profile
+ databaseHelper:database_helper_
+ storageHelper:local_storage_helper_
+ appcacheHelper:appcache_helper_]);
+
+ // Root --> foo.com --> Cookies.
+ NSMutableArray* cocoa_children =
+ [[[[[[controller_ cocoaTreeModel] children] objectAtIndex:0]
+ children] objectAtIndex:0] mutableChildren];
+ EXPECT_EQ(3U, [cocoa_children count]);
+
+ CookiesTreeModel* model = [controller_ treeModel];
+ // Root --> foo.com --> Cookies.
+ CookieTreeNode* parent = model->GetRoot()->GetChild(0)->GetChild(0);
+
+ // Pretend to remove the nodes.
+ [controller_ modelObserver]->TreeNodesRemoved(model, parent, 1, 2);
+
+ EXPECT_EQ(1U, [cocoa_children count]);
+
+ NSString* title = [[[cocoa_children objectAtIndex:0] details] name];
+ EXPECT_NSEQ(@"A", title);
+}
+
+TEST_F(CookiesWindowControllerTest, TreeNodeChanged) {
+ const GURL url = GURL("http://foo.com");
+ TestingProfile* profile = browser_helper_.profile();
+ net::CookieMonster* cm = profile->GetCookieMonster();
+ cm->SetCookie(url, "A=B");
+
+ controller_.reset(
+ [[CookiesWindowController alloc] initWithProfile:profile
+ databaseHelper:database_helper_
+ storageHelper:local_storage_helper_
+ appcacheHelper:appcache_helper_]);
+
+ CookiesTreeModel* model = [controller_ treeModel];
+ // Root --> foo.com --> Cookies.
+ CookieTreeNode* node = model->GetRoot()->GetChild(0)->GetChild(0);
+
+ // Root --> foo.com --> Cookies.
+ CocoaCookieTreeNode* cocoa_node =
+ [[[[[controller_ cocoaTreeModel] children] objectAtIndex:0]
+ children] objectAtIndex:0];
+
+ EXPECT_NSEQ(l10n_util::GetNSString(IDS_COOKIES_COOKIES),
+ [cocoa_node title]);
+
+ // Fake update the cookie folder's title. This would never happen in reality,
+ // but it tests the code path that ultimately calls CocoaNodeFromTreeNode,
+ // which is tested elsewhere.
+ node->SetTitle(ASCIIToUTF16("Silly Change"));
+ [controller_ modelObserver]->TreeNodeChanged(model, node);
+
+ EXPECT_NSEQ(@"Silly Change", [cocoa_node title]);
+}
+
+TEST_F(CookiesWindowControllerTest, DeleteCookie) {
+ const GURL url = GURL("http://foo.com");
+ TestingProfile* profile = browser_helper_.profile();
+ net::CookieMonster* cm = profile->GetCookieMonster();
+ cm->SetCookie(url, "A=B");
+ cm->SetCookie(url, "C=D");
+ cm->SetCookie(GURL("http://google.com"), "E=F");
+
+ // This will clean itself up when we call |-closeSheet:|. If we reset the
+ // scoper, we'd get a double-free.
+ CookiesWindowController* controller =
+ [[CookiesWindowController alloc] initWithProfile:profile
+ databaseHelper:database_helper_
+ storageHelper:local_storage_helper_
+ appcacheHelper:appcache_helper_];
+ [controller attachSheetTo:test_window()];
+ NSTreeController* treeController = [controller treeController];
+
+ // Select cookie A.
+ NSUInteger pathA[3] = {0, 0, 0};
+ NSIndexPath* indexPath = [NSIndexPath indexPathWithIndexes:pathA length:3];
+ [treeController setSelectionIndexPath:indexPath];
+
+ // Press the "Delete" button.
+ [controller deleteCookie:nil];
+
+ // Root --> foo.com --> Cookies.
+ NSArray* cookies = [[[[[[controller cocoaTreeModel] children]
+ objectAtIndex:0] children] objectAtIndex:0] children];
+ EXPECT_EQ(1U, [cookies count]);
+ EXPECT_NSEQ(@"C", [[cookies lastObject] title]);
+ EXPECT_NSEQ(indexPath, [treeController selectionIndexPath]);
+
+ // Select cookie E.
+ NSUInteger pathE[3] = {1, 0, 0};
+ indexPath = [NSIndexPath indexPathWithIndexes:pathE length:3];
+ [treeController setSelectionIndexPath:indexPath];
+
+ // Perform delete.
+ [controller deleteCookie:nil];
+
+ // Make sure that both the domain level node and the Cookies folder node got
+ // deleted because there was only one leaf node.
+ EXPECT_EQ(1U, [[[controller cocoaTreeModel] children] count]);
+
+ // Select cookie C.
+ NSUInteger pathC[3] = {0, 0, 0};
+ indexPath = [NSIndexPath indexPathWithIndexes:pathC length:3];
+ [treeController setSelectionIndexPath:indexPath];
+
+ // Perform delete.
+ [controller deleteCookie:nil];
+
+ // Make sure the world didn't explode and that there's nothing in the tree.
+ EXPECT_EQ(0U, [[[controller cocoaTreeModel] children] count]);
+
+ [controller closeSheet:nil];
+}
+
+TEST_F(CookiesWindowControllerTest, DidExpandItem) {
+ const GURL url = GURL("http://foo.com");
+ TestingProfile* profile = browser_helper_.profile();
+ net::CookieMonster* cm = profile->GetCookieMonster();
+ cm->SetCookie(url, "A=B");
+ cm->SetCookie(url, "C=D");
+
+ controller_.reset(
+ [[CookiesWindowController alloc] initWithProfile:profile
+ databaseHelper:database_helper_
+ storageHelper:local_storage_helper_
+ appcacheHelper:appcache_helper_]);
+
+ // Root --> foo.com.
+ CocoaCookieTreeNode* foo =
+ [[[controller_ cocoaTreeModel] children] objectAtIndex:0];
+
+ // Create the objects we are going to be testing with.
+ id outlineView = [OCMockObject mockForClass:[NSOutlineView class]];
+ id treeNode = [OCMockObject mockForClass:[NSTreeNode class]];
+ NSTreeNode* childTreeNode =
+ [NSTreeNode treeNodeWithRepresentedObject:[[foo children] lastObject]];
+ NSArray* fakeChildren = [NSArray arrayWithObject:childTreeNode];
+
+ // Set up the mock object.
+ [[[treeNode stub] andReturn:foo] representedObject];
+ [[[treeNode stub] andReturn:fakeChildren] childNodes];
+
+ // Create a fake "ItemDidExpand" notification.
+ NSDictionary* userInfo = [NSDictionary dictionaryWithObject:treeNode
+ forKey:@"NSObject"];
+ NSNotification* notif =
+ [NSNotification notificationWithName:@"ItemDidExpandNotification"
+ object:outlineView
+ userInfo:userInfo];
+
+ // Make sure we work correctly.
+ [[outlineView expect] expandItem:childTreeNode];
+ [controller_ outlineViewItemDidExpand:notif];
+ [outlineView verify];
+}
+
+TEST_F(CookiesWindowControllerTest, ClearBrowsingData) {
+ const GURL url = GURL("http://foo.com");
+ TestingProfile* profile = browser_helper_.profile();
+ net::CookieMonster* cm = profile->GetCookieMonster();
+ cm->SetCookie(url, "A=B");
+ cm->SetCookie(url, "C=D");
+ cm->SetCookie(url, "E=F");
+
+ id mock = [OCMockObject partialMockForObject:controller_.get()];
+ [[mock expect] loadTreeModelFromProfile];
+
+ NSNumber* mask =
+ [NSNumber numberWithInt:BrowsingDataRemover::REMOVE_COOKIES];
+ NSDictionary* userInfo =
+ [NSDictionary dictionaryWithObject:mask
+ forKey:kClearBrowsingDataControllerRemoveMask];
+ NSNotification* notif =
+ [NSNotification notificationWithName:kClearBrowsingDataControllerDidDelete
+ object:nil
+ userInfo:userInfo];
+ [controller_ clearBrowsingDataNotification:notif];
+
+ [mock verify];
+}
+
+// This test has been flaky under Valgrind and turns the bot red since r38504.
+// Under Mac Tests 10.5, it occasionally reports:
+// malloc: *** error for object 0x31e0468: Non-aligned pointer being freed
+// *** set a breakpoint in malloc_error_break to debug
+// Attempts to reproduce locally were not successful. This code is likely
+// changing in the future, so it's marked flaky for now. http://crbug.com/35327
+TEST_F(CookiesWindowControllerTest, FLAKY_RemoveButtonEnabled) {
+ const GURL url = GURL("http://foo.com");
+ TestingProfile* profile = browser_helper_.profile();
+ net::CookieMonster* cm = profile->GetCookieMonster();
+ cm->SetCookie(url, "A=B");
+ cm->SetCookie(url, "C=D");
+
+ // This will clean itself up when we call |-closeSheet:|. If we reset the
+ // scoper, we'd get a double-free.
+ database_helper_ = new MockBrowsingDataDatabaseHelper(profile);
+ local_storage_helper_ = new MockBrowsingDataLocalStorageHelper(profile);
+ local_storage_helper_->AddLocalStorageSamples();
+ CookiesWindowController* controller =
+ [[CookiesWindowController alloc] initWithProfile:profile
+ databaseHelper:database_helper_
+ storageHelper:local_storage_helper_
+ appcacheHelper:appcache_helper_];
+ local_storage_helper_->Notify();
+ [controller attachSheetTo:test_window()];
+
+ // Nothing should be selected right now.
+ EXPECT_FALSE([controller removeButtonEnabled]);
+
+ {
+ // Pretend to select cookie A.
+ NSUInteger path[3] = {0, 0, 0};
+ NSIndexPath* indexPath = [NSIndexPath indexPathWithIndexes:path length:3];
+ [[controller treeController] setSelectionIndexPath:indexPath];
+ [controller outlineViewSelectionDidChange:nil];
+ EXPECT_TRUE([controller removeButtonEnabled]);
+ }
+
+ {
+ // Pretend to select cookie C.
+ NSUInteger path[3] = {0, 0, 1};
+ NSIndexPath* indexPath = [NSIndexPath indexPathWithIndexes:path length:3];
+ [[controller treeController] setSelectionIndexPath:indexPath];
+ [controller outlineViewSelectionDidChange:nil];
+ EXPECT_TRUE([controller removeButtonEnabled]);
+ }
+
+ {
+ // Select a local storage node.
+ NSUInteger path[3] = {2, 0, 0};
+ NSIndexPath* indexPath = [NSIndexPath indexPathWithIndexes:path length:3];
+ [[controller treeController] setSelectionIndexPath:indexPath];
+ [controller outlineViewSelectionDidChange:nil];
+ EXPECT_TRUE([controller removeButtonEnabled]);
+ }
+
+ {
+ // Pretend to select something that isn't there!
+ NSUInteger path[3] = {0, 0, 2};
+ NSIndexPath* indexPath = [NSIndexPath indexPathWithIndexes:path length:3];
+ [[controller treeController] setSelectionIndexPath:indexPath];
+ [controller outlineViewSelectionDidChange:nil];
+ EXPECT_FALSE([controller removeButtonEnabled]);
+ }
+
+ {
+ // Try selecting something that doesn't exist again.
+ NSUInteger path[3] = {7, 1, 4};
+ NSIndexPath* indexPath = [NSIndexPath indexPathWithIndexes:path length:3];
+ [[controller treeController] setSelectionIndexPath:indexPath];
+ [controller outlineViewSelectionDidChange:nil];
+ EXPECT_FALSE([controller removeButtonEnabled]);
+ }
+
+ [controller closeSheet:nil];
+}
+
+TEST_F(CookiesWindowControllerTest, UpdateFilter) {
+ const GURL url = GURL("http://foo.com");
+ TestingProfile* profile = browser_helper_.profile();
+ net::CookieMonster* cm = profile->GetCookieMonster();
+ cm->SetCookie(GURL("http://a.com"), "A=B");
+ cm->SetCookie(GURL("http://aa.com"), "C=D");
+ cm->SetCookie(GURL("http://b.com"), "E=F");
+ cm->SetCookie(GURL("http://d.com"), "G=H");
+ cm->SetCookie(GURL("http://dd.com"), "I=J");
+
+ controller_.reset(
+ [[CookiesWindowController alloc] initWithProfile:profile
+ databaseHelper:database_helper_
+ storageHelper:local_storage_helper_
+ appcacheHelper:appcache_helper_]);
+
+ // Make sure we registered all five cookies.
+ EXPECT_EQ(5U, [[[controller_ cocoaTreeModel] children] count]);
+
+ NSSearchField* field =
+ [[NSSearchField alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)];
+
+ // Make sure we still have five cookies.
+ [field setStringValue:@""];
+ [controller_ updateFilter:field];
+ EXPECT_EQ(5U, [[[controller_ cocoaTreeModel] children] count]);
+
+ // Search for "a".
+ [field setStringValue:@"a"];
+ [controller_ updateFilter:field];
+ EXPECT_EQ(2U, [[[controller_ cocoaTreeModel] children] count]);
+
+ // Search for "b".
+ [field setStringValue:@"b"];
+ [controller_ updateFilter:field];
+ EXPECT_EQ(1U, [[[controller_ cocoaTreeModel] children] count]);
+
+ // Search for "d".
+ [field setStringValue:@"d"];
+ [controller_ updateFilter:field];
+ EXPECT_EQ(2U, [[[controller_ cocoaTreeModel] children] count]);
+
+ // Search for "e".
+ [field setStringValue:@"e"];
+ [controller_ updateFilter:field];
+ EXPECT_EQ(0U, [[[controller_ cocoaTreeModel] children] count]);
+
+ // Search for "aa".
+ [field setStringValue:@"aa"];
+ [controller_ updateFilter:field];
+ EXPECT_EQ(1U, [[[controller_ cocoaTreeModel] children] count]);
+}
+
+TEST_F(CookiesWindowControllerTest, CreateDatabaseStorageNodes) {
+ TestingProfile* profile = browser_helper_.profile();
+ database_helper_ = new MockBrowsingDataDatabaseHelper(profile);
+ local_storage_helper_ = new MockBrowsingDataLocalStorageHelper(profile);
+ database_helper_->AddDatabaseSamples();
+ controller_.reset(
+ [[CookiesWindowController alloc] initWithProfile:profile
+ databaseHelper:database_helper_
+ storageHelper:local_storage_helper_
+ appcacheHelper:appcache_helper_]);
+ database_helper_->Notify();
+
+ ASSERT_EQ(2U, [[[controller_ cocoaTreeModel] children] count]);
+
+ // Root --> gdbhost1.
+ CocoaCookieTreeNode* node =
+ [[[controller_ cocoaTreeModel] children] objectAtIndex:0];
+ EXPECT_NSEQ(@"gdbhost1", [node title]);
+ EXPECT_EQ(kCocoaCookieDetailsTypeFolder, [node nodeType]);
+ EXPECT_EQ(1U, [[node children] count]);
+
+ // host1 --> Web Databases.
+ node = [[node children] lastObject];
+ EXPECT_NSEQ(l10n_util::GetNSString(IDS_COOKIES_WEB_DATABASES), [node title]);
+ EXPECT_EQ(kCocoaCookieDetailsTypeFolder, [node nodeType]);
+ EXPECT_EQ(1U, [[node children] count]);
+
+ // Database Storage --> db1.
+ node = [[node children] lastObject];
+ EXPECT_NSEQ(@"db1", [node title]);
+ EXPECT_EQ(kCocoaCookieDetailsTypeTreeDatabase, [node nodeType]);
+ CocoaCookieDetails* details = [node details];
+ EXPECT_NSEQ(@"description 1", [details databaseDescription]);
+ EXPECT_TRUE([details lastModified]);
+ EXPECT_TRUE([details fileSize]);
+
+ // Root --> gdbhost2.
+ node =
+ [[[controller_ cocoaTreeModel] children] objectAtIndex:1];
+ EXPECT_NSEQ(@"gdbhost2", [node title]);
+ EXPECT_EQ(kCocoaCookieDetailsTypeFolder, [node nodeType]);
+ EXPECT_EQ(1U, [[node children] count]);
+
+ // host1 --> Web Databases.
+ node = [[node children] lastObject];
+ EXPECT_NSEQ(l10n_util::GetNSString(IDS_COOKIES_WEB_DATABASES), [node title]);
+ EXPECT_EQ(kCocoaCookieDetailsTypeFolder, [node nodeType]);
+ EXPECT_EQ(1U, [[node children] count]);
+
+ // Database Storage --> db2.
+ node = [[node children] lastObject];
+ EXPECT_NSEQ(@"db2", [node title]);
+ EXPECT_EQ(kCocoaCookieDetailsTypeTreeDatabase, [node nodeType]);
+ details = [node details];
+ EXPECT_NSEQ(@"description 2", [details databaseDescription]);
+ EXPECT_TRUE([details lastModified]);
+ EXPECT_TRUE([details fileSize]);
+}
+
+TEST_F(CookiesWindowControllerTest, CreateLocalStorageNodes) {
+ TestingProfile* profile = browser_helper_.profile();
+ net::CookieMonster* cm = profile->GetCookieMonster();
+ cm->SetCookie(GURL("http://google.com"), "A=B");
+ cm->SetCookie(GURL("http://dev.chromium.org"), "C=D");
+ database_helper_ = new MockBrowsingDataDatabaseHelper(profile);
+ local_storage_helper_ = new MockBrowsingDataLocalStorageHelper(profile);
+ local_storage_helper_->AddLocalStorageSamples();
+ controller_.reset(
+ [[CookiesWindowController alloc] initWithProfile:profile
+ databaseHelper:database_helper_
+ storageHelper:local_storage_helper_
+ appcacheHelper:appcache_helper_]);
+ local_storage_helper_->Notify();
+
+ ASSERT_EQ(4U, [[[controller_ cocoaTreeModel] children] count]);
+
+ // Root --> host1.
+ CocoaCookieTreeNode* node =
+ [[[controller_ cocoaTreeModel] children] objectAtIndex:2];
+ EXPECT_NSEQ(@"host1", [node title]);
+ EXPECT_EQ(kCocoaCookieDetailsTypeFolder, [node nodeType]);
+ EXPECT_EQ(1U, [[node children] count]);
+
+ // host1 --> Local Storage.
+ node = [[node children] lastObject];
+ EXPECT_NSEQ(l10n_util::GetNSString(IDS_COOKIES_LOCAL_STORAGE), [node title]);
+ EXPECT_EQ(kCocoaCookieDetailsTypeFolder, [node nodeType]);
+ EXPECT_EQ(1U, [[node children] count]);
+
+ // Local Storage --> http://host1:1/.
+ node = [[node children] lastObject];
+ EXPECT_NSEQ(@"http://host1:1/", [node title]);
+ EXPECT_EQ(kCocoaCookieDetailsTypeTreeLocalStorage, [node nodeType]);
+ EXPECT_NSEQ(@"http://host1:1/", [[node details] domain]);
+ EXPECT_TRUE([[node details] lastModified]);
+ EXPECT_TRUE([[node details] fileSize]);
+
+ // Root --> host2.
+ node =
+ [[[controller_ cocoaTreeModel] children] objectAtIndex:3];
+ EXPECT_NSEQ(@"host2", [node title]);
+ EXPECT_EQ(kCocoaCookieDetailsTypeFolder, [node nodeType]);
+ EXPECT_EQ(1U, [[node children] count]);
+
+ // host2 --> Local Storage.
+ node = [[node children] lastObject];
+ EXPECT_NSEQ(l10n_util::GetNSString(IDS_COOKIES_LOCAL_STORAGE), [node title]);
+ EXPECT_EQ(kCocoaCookieDetailsTypeFolder, [node nodeType]);
+ EXPECT_EQ(1U, [[node children] count]);
+
+ // Local Storage --> http://host2:2/.
+ node = [[node children] lastObject];
+ EXPECT_NSEQ(@"http://host2:2/", [node title]);
+ EXPECT_EQ(kCocoaCookieDetailsTypeTreeLocalStorage, [node nodeType]);
+ EXPECT_NSEQ(@"http://host2:2/", [[node details] domain]);
+ EXPECT_TRUE([[node details] lastModified]);
+ EXPECT_TRUE([[node details] fileSize]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/custom_home_pages_model.h b/chrome/browser/ui/cocoa/custom_home_pages_model.h
new file mode 100644
index 0000000..2bb94d8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/custom_home_pages_model.h
@@ -0,0 +1,91 @@
+// 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_UI_COCOA_CUSTOM_HOME_PAGES_MODEL_H_
+#define CHROME_BROWSER_UI_COCOA_CUSTOM_HOME_PAGES_MODEL_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include <vector>
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/history/history.h"
+#include "googleurl/src/gurl.h"
+
+class Profile;
+
+// The model for the "custom home pages" table in preferences. Contains a list
+// of CustomHomePageEntry objects. This is intended to be used with Cocoa
+// bindings.
+//
+// The supported binding is |customHomePages|, a to-many relationship which
+// can be observed with an array controller.
+
+@interface CustomHomePagesModel : NSObject {
+ @private
+ scoped_nsobject<NSMutableArray> entries_;
+ Profile* profile_; // weak, used for loading favicons
+}
+
+// Initialize with |profile|, which must not be NULL. The profile is used for
+// loading favicons for urls.
+- (id)initWithProfile:(Profile*)profile;
+
+// Get/set the urls the model currently contains as a group. Only one change
+// notification will be sent.
+- (std::vector<GURL>)URLs;
+- (void)setURLs:(const std::vector<GURL>&)urls;
+
+// Reloads the URLs from their stored state. This will notify using KVO
+// |customHomePages|.
+- (void)reloadURLs;
+
+// Validates the set of URLs stored in the model. The user may have input bad
+// data. This function removes invalid entries from the model, which will result
+// in anyone observing being updated.
+- (void)validateURLs;
+
+// For binding |customHomePages| to a mutable array controller.
+- (NSUInteger)countOfCustomHomePages;
+- (id)objectInCustomHomePagesAtIndex:(NSUInteger)index;
+- (void)insertObject:(id)object inCustomHomePagesAtIndex:(NSUInteger)index;
+- (void)removeObjectFromCustomHomePagesAtIndex:(NSUInteger)index;
+@end
+
+////////////////////////////////////////////////////////////////////////////////
+
+// An entry representing a single item in the custom home page model. Stores
+// a url and a favicon.
+@interface CustomHomePageEntry : NSObject {
+ @private
+ scoped_nsobject<NSString> url_;
+ scoped_nsobject<NSImage> icon_;
+
+ // If non-zero, indicates we're loading the favicon for the page.
+ HistoryService::Handle icon_handle_;
+}
+
+@property(nonatomic, copy) NSString* URL;
+@property(nonatomic, retain) NSImage* image;
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////
+
+@interface CustomHomePagesModel (InternalOrTestingAPI)
+
+// Clears the URL string at the specified index. This constitutes bad data. The
+// validator should scrub the entry from the list the next time it is run.
+- (void)setURLStringEmptyAt:(NSUInteger)index;
+
+@end
+
+// A notification that fires when the URL of one of the entries changes.
+// Prevents interested parties from having to observe all model objects in order
+// to persist changes to a single entry. Changes to the number of items in the
+// model can be observed by watching |customHomePages| via KVO so an additional
+// notification is not sent.
+extern NSString* const kHomepageEntryChangedNotification;
+
+#endif // CHROME_BROWSER_UI_COCOA_CUSTOM_HOME_PAGES_MODEL_H_
diff --git a/chrome/browser/ui/cocoa/custom_home_pages_model.mm b/chrome/browser/ui/cocoa/custom_home_pages_model.mm
new file mode 100644
index 0000000..2e0be88
--- /dev/null
+++ b/chrome/browser/ui/cocoa/custom_home_pages_model.mm
@@ -0,0 +1,140 @@
+// 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/ui/cocoa/custom_home_pages_model.h"
+
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/net/url_fixer_upper.h"
+#include "chrome/browser/prefs/session_startup_pref.h"
+
+NSString* const kHomepageEntryChangedNotification =
+ @"kHomepageEntryChangedNotification";
+
+@interface CustomHomePagesModel (Private)
+- (void)setURLsInternal:(const std::vector<GURL>&)urls;
+@end
+
+@implementation CustomHomePagesModel
+
+- (id)initWithProfile:(Profile*)profile {
+ if ((self = [super init])) {
+ profile_ = profile;
+ entries_.reset([[NSMutableArray alloc] init]);
+ }
+ return self;
+}
+
+- (NSUInteger)countOfCustomHomePages {
+ return [entries_ count];
+}
+
+- (id)objectInCustomHomePagesAtIndex:(NSUInteger)index {
+ return [entries_ objectAtIndex:index];
+}
+
+- (void)insertObject:(id)object inCustomHomePagesAtIndex:(NSUInteger)index {
+ [entries_ insertObject:object atIndex:index];
+}
+
+- (void)removeObjectFromCustomHomePagesAtIndex:(NSUInteger)index {
+ [entries_ removeObjectAtIndex:index];
+ // Force a save.
+ [self validateURLs];
+}
+
+// Get/set the urls the model currently contains as a group. These will weed
+// out any URLs that are empty and not add them to the model. As a result,
+// the next time they're persisted to the prefs backend, they'll disappear.
+- (std::vector<GURL>)URLs {
+ std::vector<GURL> urls;
+ for (CustomHomePageEntry* entry in entries_.get()) {
+ const char* urlString = [[entry URL] UTF8String];
+ if (urlString && std::strlen(urlString)) {
+ urls.push_back(GURL(std::string(urlString)));
+ }
+ }
+ return urls;
+}
+
+- (void)setURLs:(const std::vector<GURL>&)urls {
+ [self willChangeValueForKey:@"customHomePages"];
+ [self setURLsInternal:urls];
+ SessionStartupPref pref(SessionStartupPref::GetStartupPref(profile_));
+ pref.urls = urls;
+ SessionStartupPref::SetStartupPref(profile_, pref);
+ [self didChangeValueForKey:@"customHomePages"];
+}
+
+// Converts the C++ URLs to Cocoa objects without notifying KVO.
+- (void)setURLsInternal:(const std::vector<GURL>&)urls {
+ [entries_ removeAllObjects];
+ for (size_t i = 0; i < urls.size(); ++i) {
+ scoped_nsobject<CustomHomePageEntry> entry(
+ [[CustomHomePageEntry alloc] init]);
+ const char* urlString = urls[i].spec().c_str();
+ if (urlString && std::strlen(urlString)) {
+ [entry setURL:[NSString stringWithCString:urlString
+ encoding:NSUTF8StringEncoding]];
+ [entries_ addObject:entry];
+ }
+ }
+}
+
+- (void)reloadURLs {
+ [self willChangeValueForKey:@"customHomePages"];
+ SessionStartupPref pref(SessionStartupPref::GetStartupPref(profile_));
+ [self setURLsInternal:pref.urls];
+ [self didChangeValueForKey:@"customHomePages"];
+}
+
+- (void)validateURLs {
+ [self setURLs:[self URLs]];
+}
+
+- (void)setURLStringEmptyAt:(NSUInteger)index {
+ // This replaces the data at |index| with an empty (invalid) URL string.
+ CustomHomePageEntry* entry = [entries_ objectAtIndex:index];
+ [entry setURL:[NSString stringWithString:@""]];
+}
+
+@end
+
+//---------------------------------------------------------------------------
+
+@implementation CustomHomePageEntry
+
+- (void)setURL:(NSString*)url {
+ // |url| can be nil if the user cleared the text from the edit field.
+ if (!url)
+ url = [NSString stringWithString:@""];
+
+ // Make sure the url is valid before setting it by fixing it up.
+ std::string fixedUrl(URLFixerUpper::FixupURL(
+ base::SysNSStringToUTF8(url), std::string()).possibly_invalid_spec());
+ url_.reset([base::SysUTF8ToNSString(fixedUrl) retain]);
+
+ // Broadcast that an individual item has changed.
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kHomepageEntryChangedNotification object:nil];
+
+ // TODO(pinkerton): fetch favicon, convert to NSImage http://crbug.com/34642
+}
+
+- (NSString*)URL {
+ return url_.get();
+}
+
+- (void)setImage:(NSImage*)image {
+ icon_.reset(image);
+}
+
+- (NSImage*)image {
+ return icon_.get();
+}
+
+- (NSString*)description {
+ return url_.get();
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/custom_home_pages_model_unittest.mm b/chrome/browser/ui/cocoa/custom_home_pages_model_unittest.mm
new file mode 100644
index 0000000..c744775
--- /dev/null
+++ b/chrome/browser/ui/cocoa/custom_home_pages_model_unittest.mm
@@ -0,0 +1,196 @@
+// 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"
+#include "chrome/browser/prefs/session_startup_pref.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/custom_home_pages_model.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+// A helper for KVO and NSNotifications. Makes a note that it's been called
+// back.
+@interface CustomHomePageHelper : NSObject {
+ @public
+ BOOL sawNotification_;
+}
+@end
+
+@implementation CustomHomePageHelper
+- (void)observeValueForKeyPath:(NSString*)keyPath
+ ofObject:(id)object
+ change:(NSDictionary*)change
+ context:(void*)context {
+ sawNotification_ = YES;
+}
+
+- (void)entryChanged:(NSNotification*)notify {
+ sawNotification_ = YES;
+}
+@end
+
+@interface NSObject ()
+- (void)setURL:(NSString*)url;
+@end
+
+namespace {
+
+// Helper that creates an autoreleased entry.
+CustomHomePageEntry* MakeEntry(NSString* url) {
+ CustomHomePageEntry* entry = [[[CustomHomePageEntry alloc] init] autorelease];
+ [entry setURL:url];
+ return entry;
+}
+
+// Helper that casts from |id| to the Entry type and returns the URL string.
+NSString* EntryURL(id entry) {
+ return [static_cast<CustomHomePageEntry*>(entry) URL];
+}
+
+class CustomHomePagesModelTest : public PlatformTest {
+ public:
+ CustomHomePagesModelTest() {
+ model_.reset([[CustomHomePagesModel alloc]
+ initWithProfile:helper_.profile()]);
+ }
+ ~CustomHomePagesModelTest() { }
+
+ BrowserTestHelper helper_;
+ scoped_nsobject<CustomHomePagesModel> model_;
+};
+
+TEST_F(CustomHomePagesModelTest, Init) {
+ scoped_nsobject<CustomHomePagesModel> model(
+ [[CustomHomePagesModel alloc] initWithProfile:helper_.profile()]);
+}
+
+TEST_F(CustomHomePagesModelTest, GetSetURLs) {
+ // Basic test.
+ std::vector<GURL> urls;
+ urls.push_back(GURL("http://www.google.com"));
+ [model_ setURLs:urls];
+ std::vector<GURL> received_urls = [model_.get() URLs];
+ EXPECT_EQ(received_urls.size(), 1U);
+ EXPECT_TRUE(urls[0] == received_urls[0]);
+
+ // Set an empty list, make sure we get back an empty list.
+ std::vector<GURL> empty;
+ [model_ setURLs:empty];
+ received_urls = [model_.get() URLs];
+ EXPECT_EQ(received_urls.size(), 0U);
+
+ // Give it a list with not well-formed URLs and make sure we get back.
+ // only the good ones.
+ std::vector<GURL> poorly_formed;
+ poorly_formed.push_back(GURL("http://www.google.com")); // good
+ poorly_formed.push_back(GURL("www.google.com")); // bad
+ poorly_formed.push_back(GURL("www.yahoo.")); // bad
+ poorly_formed.push_back(GURL("http://www.yahoo.com")); // good
+ [model_ setURLs:poorly_formed];
+ received_urls = [model_.get() URLs];
+ EXPECT_EQ(received_urls.size(), 2U);
+}
+
+// Test that we get a KVO notification when called setURLs.
+TEST_F(CustomHomePagesModelTest, KVOObserveWhenListChanges) {
+ scoped_nsobject<CustomHomePageHelper> kvo_helper(
+ [[CustomHomePageHelper alloc] init]);
+ [model_ addObserver:kvo_helper
+ forKeyPath:@"customHomePages"
+ options:0L
+ context:NULL];
+ EXPECT_FALSE(kvo_helper.get()->sawNotification_);
+
+ std::vector<GURL> urls;
+ urls.push_back(GURL("http://www.google.com"));
+ [model_ setURLs:urls]; // Should send kvo change notification.
+ EXPECT_TRUE(kvo_helper.get()->sawNotification_);
+
+ [model_ removeObserver:kvo_helper forKeyPath:@"customHomePages"];
+}
+
+// Test the KVO "to-many" bindings for |customHomePages| and the KVO
+// notifiation when items are added to and removed from the list.
+TEST_F(CustomHomePagesModelTest, KVO) {
+ EXPECT_EQ([model_ countOfCustomHomePages], 0U);
+
+ scoped_nsobject<CustomHomePageHelper> kvo_helper(
+ [[CustomHomePageHelper alloc] init]);
+ [model_ addObserver:kvo_helper
+ forKeyPath:@"customHomePages"
+ options:0L
+ context:NULL];
+ EXPECT_FALSE(kvo_helper.get()->sawNotification_);
+
+ // Cheat and insert NSString objects into the array. As long as we don't
+ // call -URLs, we'll be ok.
+ [model_ insertObject:MakeEntry(@"www.google.com") inCustomHomePagesAtIndex:0];
+ EXPECT_TRUE(kvo_helper.get()->sawNotification_);
+ [model_ insertObject:MakeEntry(@"www.yahoo.com") inCustomHomePagesAtIndex:1];
+ [model_ insertObject:MakeEntry(@"dev.chromium.org")
+ inCustomHomePagesAtIndex:2];
+ EXPECT_EQ([model_ countOfCustomHomePages], 3U);
+
+ EXPECT_NSEQ(@"http://www.yahoo.com/",
+ EntryURL([model_ objectInCustomHomePagesAtIndex:1]));
+
+ kvo_helper.get()->sawNotification_ = NO;
+ [model_ removeObjectFromCustomHomePagesAtIndex:1];
+ EXPECT_TRUE(kvo_helper.get()->sawNotification_);
+ EXPECT_EQ([model_ countOfCustomHomePages], 2U);
+ EXPECT_NSEQ(@"http://dev.chromium.org/",
+ EntryURL([model_ objectInCustomHomePagesAtIndex:1]));
+ EXPECT_NSEQ(@"http://www.google.com/",
+ EntryURL([model_ objectInCustomHomePagesAtIndex:0]));
+
+ [model_ removeObserver:kvo_helper forKeyPath:@"customHomePages"];
+}
+
+// Test that when individual items are changed that they broadcast a message.
+TEST_F(CustomHomePagesModelTest, ModelChangedNotification) {
+ scoped_nsobject<CustomHomePageHelper> kvo_helper(
+ [[CustomHomePageHelper alloc] init]);
+ [[NSNotificationCenter defaultCenter]
+ addObserver:kvo_helper
+ selector:@selector(entryChanged:)
+ name:kHomepageEntryChangedNotification
+ object:nil];
+
+ std::vector<GURL> urls;
+ urls.push_back(GURL("http://www.google.com"));
+ [model_ setURLs:urls];
+ NSObject* entry = [model_ objectInCustomHomePagesAtIndex:0];
+ [entry setURL:@"http://www.foo.bar"];
+ EXPECT_TRUE(kvo_helper.get()->sawNotification_);
+ [[NSNotificationCenter defaultCenter] removeObserver:kvo_helper];
+}
+
+TEST_F(CustomHomePagesModelTest, ReloadURLs) {
+ scoped_nsobject<CustomHomePageHelper> kvo_helper(
+ [[CustomHomePageHelper alloc] init]);
+ [model_ addObserver:kvo_helper
+ forKeyPath:@"customHomePages"
+ options:0L
+ context:NULL];
+ EXPECT_FALSE(kvo_helper.get()->sawNotification_);
+ EXPECT_EQ([model_ countOfCustomHomePages], 0U);
+
+ std::vector<GURL> urls;
+ urls.push_back(GURL("http://www.google.com"));
+ SessionStartupPref pref;
+ pref.urls = urls;
+ SessionStartupPref::SetStartupPref(helper_.profile(), pref);
+
+ [model_ reloadURLs];
+
+ EXPECT_TRUE(kvo_helper.get()->sawNotification_);
+ EXPECT_EQ([model_ countOfCustomHomePages], 1U);
+ EXPECT_NSEQ(@"http://www.google.com/",
+ EntryURL([model_ objectInCustomHomePagesAtIndex:0]));
+
+ [model_ removeObserver:kvo_helper.get() forKeyPath:@"customHomePages"];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/delayedmenu_button.h b/chrome/browser/ui/cocoa/delayedmenu_button.h
new file mode 100644
index 0000000..6363d30
--- /dev/null
+++ b/chrome/browser/ui/cocoa/delayedmenu_button.h
@@ -0,0 +1,32 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_DELAYEDMENU_BUTTON_H_
+#define CHROME_BROWSER_UI_COCOA_DELAYEDMENU_BUTTON_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+
+@interface DelayedMenuButton : NSButton {
+ NSMenu* attachedMenu_; // Strong (retained).
+ BOOL attachedMenuEnabled_;
+ scoped_nsobject<NSPopUpButtonCell> popUpCell_;
+}
+
+// The menu to display. Note that it should have no (i.e., a blank) title and
+// that the 0-th entry should be blank (and won't be displayed). (This is
+// because we use a pulldown list, for which Cocoa uses the 0-th item as "title"
+// in the button. This might change if we ever switch to a pop-up. Our direct
+// use of the given NSMenu object means that the one can set and use NSMenu's
+// delegate as usual.)
+@property(retain, nonatomic) NSMenu* attachedMenu;
+
+// Is the menu enabled? (If not, don't act like a click-hold button.)
+@property(assign, nonatomic) BOOL attachedMenuEnabled;
+
+@end // @interface DelayedMenuButton
+
+#endif // CHROME_BROWSER_UI_COCOA_DELAYEDMENU_BUTTON_H_
diff --git a/chrome/browser/ui/cocoa/delayedmenu_button.mm b/chrome/browser/ui/cocoa/delayedmenu_button.mm
new file mode 100644
index 0000000..9a9d73d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/delayedmenu_button.mm
@@ -0,0 +1,137 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/delayedmenu_button.h"
+
+#include "base/logging.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/clickhold_button_cell.h"
+
+@interface DelayedMenuButton (Private)
+
+- (void)setupCell;
+- (void)attachedMenuAction:(id)sender;
+
+@end // @interface DelayedMenuButton (Private)
+
+@implementation DelayedMenuButton
+
+// Overrides:
+
++ (Class)cellClass {
+ return [ClickHoldButtonCell class];
+}
+
+- (id)init {
+ if ((self = [super init]))
+ [self setupCell];
+ return self;
+}
+
+- (id)initWithCoder:(NSCoder*)decoder {
+ if ((self = [super initWithCoder:decoder]))
+ [self setupCell];
+ return self;
+}
+
+- (id)initWithFrame:(NSRect)frameRect {
+ if ((self = [super initWithFrame:frameRect]))
+ [self setupCell];
+ return self;
+}
+
+- (void)dealloc {
+ [attachedMenu_ release];
+ [super dealloc];
+}
+
+- (void)awakeFromNib {
+ [self setupCell];
+}
+
+- (void)setCell:(NSCell*)cell {
+ [super setCell:cell];
+ [self setupCell];
+}
+
+// Accessors and mutators:
+
+@synthesize attachedMenu = attachedMenu_;
+
+// Don't synthesize for attachedMenuEnabled_; its mutator must do other things.
+- (void)setAttachedMenuEnabled:(BOOL)enabled {
+ attachedMenuEnabled_ = enabled;
+ [[self cell] setEnableClickHold:attachedMenuEnabled_];
+}
+
+- (BOOL)attachedMenuEnabled {
+ return attachedMenuEnabled_;
+}
+
+@end // @implementation DelayedMenuButton
+
+@implementation DelayedMenuButton (Private)
+
+// Set up the button's cell if we've reached a point where it's been set.
+- (void)setupCell {
+ ClickHoldButtonCell* cell = [self cell];
+ if (cell) {
+ DCHECK([cell isKindOfClass:[ClickHoldButtonCell class]]);
+ [self setEnabled:NO]; // Make the controller put in a menu and
+ // enable it explicitly. This also takes
+ // care of |[cell setEnableClickHold:]|.
+ [cell setClickHoldAction:@selector(attachedMenuAction:)];
+ [cell setClickHoldTarget:self];
+ }
+}
+
+// Display the menu.
+- (void)attachedMenuAction:(id)sender {
+ // We shouldn't get here unless the menu is enabled.
+ DCHECK(attachedMenuEnabled_);
+
+ // If we don't have a menu (in which case the person using this control is
+ // being bad), just wait for a mouse up.
+ if (!attachedMenu_) {
+ LOG(WARNING) << "No menu available.";
+ [NSApp nextEventMatchingMask:NSLeftMouseUpMask
+ untilDate:[NSDate distantFuture]
+ inMode:NSEventTrackingRunLoopMode
+ dequeue:YES];
+ return;
+ }
+
+ // TODO(viettrungluu): We have some fudge factors below to make things line up
+ // (approximately). I wish I knew how to get rid of them. (Note that our view
+ // is flipped, and that frame should be in our coordinates.) The y/height is
+ // very odd, since it doesn't seem to respond to changes the way that it
+ // should. I don't understand it.
+ NSRect frame = [self convertRect:[self frame]
+ fromView:[self superview]];
+ frame.origin.x -= 2.0;
+ frame.size.height += 10.0;
+
+ // Make our pop-up button cell and set things up. This is, as of 10.5, the
+ // official Apple-recommended hack. Later, perhaps |-[NSMenu
+ // popUpMenuPositioningItem:atLocation:inView:]| may be a better option.
+ // However, using a pulldown has the benefit that Cocoa automatically places
+ // the menu correctly even when we're at the edge of the screen (including
+ // "dragging upwards" when the button is close to the bottom of the screen).
+ // A |scoped_nsobject| local variable cannot be used here because
+ // Accessibility on 10.5 grabs the NSPopUpButtonCell without retaining it, and
+ // uses it later. (This is fixed in 10.6.)
+ if (!popUpCell_.get()) {
+ popUpCell_.reset([[NSPopUpButtonCell alloc] initTextCell:@""
+ pullsDown:YES]);
+ }
+ DCHECK(popUpCell_.get());
+ [popUpCell_ setMenu:attachedMenu_];
+ [popUpCell_ selectItem:nil];
+ [popUpCell_ attachPopUpWithFrame:frame
+ inView:self];
+ [popUpCell_ performClickWithFrame:frame
+ inView:self];
+}
+
+@end // @implementation DelayedMenuButton (Private)
diff --git a/chrome/browser/ui/cocoa/delayedmenu_button_unittest.mm b/chrome/browser/ui/cocoa/delayedmenu_button_unittest.mm
new file mode 100644
index 0000000..d1d7b77
--- /dev/null
+++ b/chrome/browser/ui/cocoa/delayedmenu_button_unittest.mm
@@ -0,0 +1,62 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/clickhold_button_cell.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/delayedmenu_button.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class DelayedMenuButtonTest : public CocoaTest {
+ public:
+ DelayedMenuButtonTest() {
+ NSRect frame = NSMakeRect(0, 0, 50, 30);
+ scoped_nsobject<DelayedMenuButton>button([[DelayedMenuButton alloc]
+ initWithFrame:frame]);
+ button_ = button.get();
+ scoped_nsobject<ClickHoldButtonCell> cell(
+ [[ClickHoldButtonCell alloc] initTextCell:@"Testing"]);
+ [button_ setCell:cell.get()];
+ [[test_window() contentView] addSubview:button_];
+ }
+
+ DelayedMenuButton* button_;
+};
+
+TEST_VIEW(DelayedMenuButtonTest, button_)
+
+// Test assigning and enabling a menu, again mostly to ensure nothing leaks or
+// crashes.
+TEST_F(DelayedMenuButtonTest, MenuAssign) {
+ scoped_nsobject<NSMenu> menu([[NSMenu alloc] initWithTitle:@""]);
+ ASSERT_TRUE(menu.get());
+
+ [menu insertItemWithTitle:@"" action:nil keyEquivalent:@"" atIndex:0];
+ [menu insertItemWithTitle:@"foo" action:nil keyEquivalent:@"" atIndex:1];
+ [menu insertItemWithTitle:@"bar" action:nil keyEquivalent:@"" atIndex:2];
+ [menu insertItemWithTitle:@"baz" action:nil keyEquivalent:@"" atIndex:3];
+
+ [button_ setAttachedMenu:menu];
+ EXPECT_TRUE([button_ attachedMenu]);
+
+ [button_ setAttachedMenuEnabled:YES];
+ EXPECT_TRUE([button_ attachedMenuEnabled]);
+
+ // TODO(viettrungluu): Display the menu. (Calling DelayedMenuButton's private
+ // |-attachedMenuAction:| method displays it fine, but the problem is
+ // getting rid of the menu. We can catch the
+ // |NSMenuDidBeginTrackingNotification| from |menu| fine, but then
+ // |-cancelTracking| doesn't dismiss it. I don't know why.)
+}
+
+// TODO(viettrungluu): Test the two actions of the button (the normal one and
+// displaying the menu, also making sure the latter drags correctly)? It would
+// require "emulating" a mouse....
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/dev_tools_controller.h b/chrome/browser/ui/cocoa/dev_tools_controller.h
new file mode 100644
index 0000000..c89a9f8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/dev_tools_controller.h
@@ -0,0 +1,51 @@
+// 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_UI_COCOA_DEV_TOOLS_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_DEV_TOOLS_CONTROLLER_H_
+#pragma once
+
+#import <Foundation/Foundation.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/tab_contents_controller.h"
+
+@class NSSplitView;
+@class NSView;
+
+class TabContents;
+
+// A class that handles updates of the devTools view within a browser window.
+// It swaps in the relevant devTools contents for a given TabContents or removes
+// the vew, if there's no devTools contents to show.
+@interface DevToolsController : NSObject {
+ @private
+ // A view hosting docked devTools contents.
+ scoped_nsobject<NSSplitView> splitView_;
+
+ // Manages currently displayed devTools contents.
+ scoped_nsobject<TabContentsController> contentsController_;
+}
+
+- (id)initWithDelegate:(id<TabContentsControllerDelegate>)delegate;
+
+// This controller's view.
+- (NSView*)view;
+
+// The compiler seems to have trouble handling a function named "view" that
+// returns an NSSplitView, so provide a differently-named method.
+- (NSSplitView*)splitView;
+
+// Depending on |contents|'s state, decides whether the docked web inspector
+// should be shown or hidden and adjusts its height (|delegate_| handles
+// the actual resize).
+- (void)updateDevToolsForTabContents:(TabContents*)contents;
+
+// Call when the devTools view is properly sized and the render widget host view
+// should be put into the view hierarchy.
+- (void)ensureContentsVisible;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_DEV_TOOLS_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/dev_tools_controller.mm b/chrome/browser/ui/cocoa/dev_tools_controller.mm
new file mode 100644
index 0000000..596bdae
--- /dev/null
+++ b/chrome/browser/ui/cocoa/dev_tools_controller.mm
@@ -0,0 +1,164 @@
+// 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/ui/cocoa/dev_tools_controller.h"
+
+#include <algorithm>
+
+#include <Cocoa/Cocoa.h>
+
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/debugger/devtools_window.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/ui/browser.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+#include "chrome/common/pref_names.h"
+
+namespace {
+
+// Default offset of the contents splitter in pixels.
+const int kDefaultContentsSplitOffset = 400;
+
+// Never make the web part of the tab contents smaller than this (needed if the
+// window is only a few pixels high).
+const int kMinWebHeight = 50;
+
+} // end namespace
+
+
+@interface DevToolsController (Private)
+- (void)showDevToolsContents:(TabContents*)devToolsContents;
+- (void)resizeDevToolsToNewHeight:(CGFloat)height;
+@end
+
+
+@implementation DevToolsController
+
+- (id)initWithDelegate:(id<TabContentsControllerDelegate>)delegate {
+ if ((self = [super init])) {
+ splitView_.reset([[NSSplitView alloc] initWithFrame:NSZeroRect]);
+ [splitView_ setDividerStyle:NSSplitViewDividerStyleThin];
+ [splitView_ setVertical:NO];
+ [splitView_ setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
+ [splitView_ setDelegate:self];
+
+ contentsController_.reset(
+ [[TabContentsController alloc] initWithContents:NULL
+ delegate:delegate]);
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [splitView_ setDelegate:nil];
+ [super dealloc];
+}
+
+- (NSView*)view {
+ return splitView_.get();
+}
+
+- (NSSplitView*)splitView {
+ return splitView_.get();
+}
+
+- (void)updateDevToolsForTabContents:(TabContents*)contents {
+ // Get current devtools content.
+ TabContents* devToolsContents = contents ?
+ DevToolsWindow::GetDevToolsContents(contents) : NULL;
+
+ [self showDevToolsContents:devToolsContents];
+}
+
+- (void)ensureContentsVisible {
+ [contentsController_ ensureContentsVisible];
+}
+
+- (void)showDevToolsContents:(TabContents*)devToolsContents {
+ [contentsController_ ensureContentsSizeDoesNotChange];
+
+ NSArray* subviews = [splitView_ subviews];
+ if (devToolsContents) {
+ DCHECK_GE([subviews count], 1u);
+
+ // |devToolsView| is a TabContentsViewCocoa object, whose ViewID was
+ // set to VIEW_ID_TAB_CONTAINER initially, so we need to change it to
+ // VIEW_ID_DEV_TOOLS_DOCKED here.
+ view_id_util::SetID(
+ devToolsContents->GetNativeView(), VIEW_ID_DEV_TOOLS_DOCKED);
+
+ CGFloat splitOffset = 0;
+ if ([subviews count] == 1) {
+ // Load the default split offset.
+ splitOffset = g_browser_process->local_state()->GetInteger(
+ prefs::kDevToolsSplitLocation);
+ if (splitOffset < 0) {
+ // Initial load, set to default value.
+ splitOffset = kDefaultContentsSplitOffset;
+ }
+ [splitView_ addSubview:[contentsController_ view]];
+ } else {
+ DCHECK_EQ([subviews count], 2u);
+ // If devtools are already visible, keep the current size.
+ splitOffset = NSHeight([[subviews objectAtIndex:1] frame]);
+ }
+
+ // Make sure |splitOffset| isn't too large or too small.
+ splitOffset = std::max(static_cast<CGFloat>(kMinWebHeight), splitOffset);
+ splitOffset =
+ std::min(splitOffset, NSHeight([splitView_ frame]) - kMinWebHeight);
+ DCHECK_GE(splitOffset, 0) << "kMinWebHeight needs to be smaller than "
+ << "smallest available tab contents space.";
+
+ [self resizeDevToolsToNewHeight:splitOffset];
+ } else {
+ if ([subviews count] > 1) {
+ NSView* oldDevToolsContentsView = [subviews objectAtIndex:1];
+ // Store split offset when hiding devtools window only.
+ int splitOffset = NSHeight([oldDevToolsContentsView frame]);
+ g_browser_process->local_state()->SetInteger(
+ prefs::kDevToolsSplitLocation, splitOffset);
+ [oldDevToolsContentsView removeFromSuperview];
+ [splitView_ adjustSubviews];
+ }
+ }
+
+ [contentsController_ changeTabContents:devToolsContents];
+}
+
+- (void)resizeDevToolsToNewHeight:(CGFloat)height {
+ NSArray* subviews = [splitView_ subviews];
+
+ // It seems as if |-setPosition:ofDividerAtIndex:| should do what's needed,
+ // but I can't figure out how to use it. Manually resize web and devtools.
+ // TODO(alekseys): either make setPosition:ofDividerAtIndex: work or to add a
+ // category on NSSplitView to handle manual resizing.
+ NSView* devToolsView = [subviews objectAtIndex:1];
+ NSRect devToolsFrame = [devToolsView frame];
+ devToolsFrame.size.height = height;
+ [devToolsView setFrame:devToolsFrame];
+
+ NSView* webView = [subviews objectAtIndex:0];
+ NSRect webFrame = [webView frame];
+ webFrame.size.height =
+ NSHeight([splitView_ frame]) - ([splitView_ dividerThickness] + height);
+ [webView setFrame:webFrame];
+
+ [splitView_ adjustSubviews];
+}
+
+// NSSplitViewDelegate protocol.
+- (BOOL)splitView:(NSSplitView *)splitView
+ shouldAdjustSizeOfSubview:(NSView *)subview {
+ // Return NO for the devTools view to indicate that it should not be resized
+ // automatically. It preserves the height set by the user and also keeps
+ // view height the same while changing tabs when one of the tabs shows infobar
+ // and others are not.
+ if ([[splitView_ subviews] indexOfObject:subview] == 1)
+ return NO;
+ return YES;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/dock_icon.h b/chrome/browser/ui/cocoa/dock_icon.h
new file mode 100644
index 0000000..4a96537
--- /dev/null
+++ b/chrome/browser/ui/cocoa/dock_icon.h
@@ -0,0 +1,30 @@
+// 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>
+
+// A class representing the dock icon of the Chromium app. It's its own class
+// since several parts of the app want to manipulate the display of the dock
+// icon.
+@interface DockIcon : NSObject {
+}
+
++ (DockIcon*)sharedDockIcon;
+
+// Updates the icon. Use the setters below to set the details first.
+- (void)updateIcon;
+
+// Download progress ///////////////////////////////////////////////////////////
+
+// Indicates how many downloads are in progress.
+- (void)setDownloads:(int)downloads;
+
+// Indicates whether the progress indicator should be in an indeterminate state
+// or not.
+- (void)setIndeterminate:(BOOL)indeterminate;
+
+// Indicates the amount of progress made of the download. Ranges from [0..1].
+- (void)setProgress:(float)progress;
+
+@end
diff --git a/chrome/browser/ui/cocoa/dock_icon.mm b/chrome/browser/ui/cocoa/dock_icon.mm
new file mode 100644
index 0000000..980519a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/dock_icon.mm
@@ -0,0 +1,224 @@
+// 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/ui/cocoa/dock_icon.h"
+
+#include "base/scoped_nsobject.h"
+
+// The fraction of the size of the dock icon that the badge is.
+static const float kBadgeFraction = 0.4f;
+
+// The indentation of the badge.
+static const float kBadgeIndent = 5.0f;
+
+// A view that draws our dock tile.
+@interface DockTileView : NSView {
+ @private
+ int downloads_;
+ BOOL indeterminate_;
+ float progress_;
+}
+
+// Indicates how many downloads are in progress.
+@property (nonatomic) int downloads;
+
+// Indicates whether the progress indicator should be in an indeterminate state
+// or not.
+@property (nonatomic) BOOL indeterminate;
+
+// Indicates the amount of progress made of the download. Ranges from [0..1].
+@property (nonatomic) float progress;
+
+@end
+
+@implementation DockTileView
+
+@synthesize downloads = downloads_;
+@synthesize indeterminate = indeterminate_;
+@synthesize progress = progress_;
+
+- (void)drawRect:(NSRect)dirtyRect {
+ // Not -[NSApplication applicationIconImage]; that fails to return a pasted
+ // custom icon.
+ NSString* appPath = [[NSBundle mainBundle] bundlePath];
+ NSImage* appIcon = [[NSWorkspace sharedWorkspace] iconForFile:appPath];
+ [appIcon drawInRect:[self bounds]
+ fromRect:NSZeroRect
+ operation:NSCompositeSourceOver
+ fraction:1.0];
+
+ if (downloads_ == 0)
+ return;
+
+ NSRect badgeRect = [self bounds];
+ badgeRect.size.height = (int)(kBadgeFraction * badgeRect.size.height);
+ int newWidth = kBadgeFraction * badgeRect.size.width;
+ badgeRect.origin.x = badgeRect.size.width - newWidth;
+ badgeRect.size.width = newWidth;
+
+ CGFloat badgeRadius = NSMidY(badgeRect);
+
+ badgeRect.origin.x -= kBadgeIndent;
+ badgeRect.origin.y += kBadgeIndent;
+
+ NSPoint badgeCenter = NSMakePoint(NSMidX(badgeRect),
+ NSMidY(badgeRect));
+
+ // Background
+ NSColor* backgroundColor = [NSColor colorWithCalibratedRed:0.85
+ green:0.85
+ blue:0.85
+ alpha:1.0];
+ NSColor* backgroundHighlight =
+ [backgroundColor blendedColorWithFraction:0.85
+ ofColor:[NSColor whiteColor]];
+ scoped_nsobject<NSGradient> backgroundGradient(
+ [[NSGradient alloc] initWithStartingColor:backgroundHighlight
+ endingColor:backgroundColor]);
+ NSBezierPath* badgeEdge = [NSBezierPath bezierPathWithOvalInRect:badgeRect];
+ [NSGraphicsContext saveGraphicsState];
+ [badgeEdge addClip];
+ [backgroundGradient drawFromCenter:badgeCenter
+ radius:0.0
+ toCenter:badgeCenter
+ radius:badgeRadius
+ options:0];
+ [NSGraphicsContext restoreGraphicsState];
+
+ // Slice
+ if (!indeterminate_) {
+ NSColor* sliceColor = [NSColor colorWithCalibratedRed:0.45
+ green:0.8
+ blue:0.25
+ alpha:1.0];
+ NSColor* sliceHighlight =
+ [sliceColor blendedColorWithFraction:0.4
+ ofColor:[NSColor whiteColor]];
+ scoped_nsobject<NSGradient> sliceGradient(
+ [[NSGradient alloc] initWithStartingColor:sliceHighlight
+ endingColor:sliceColor]);
+ NSBezierPath* progressSlice;
+ if (progress_ >= 1.0) {
+ progressSlice = [NSBezierPath bezierPathWithOvalInRect:badgeRect];
+ } else {
+ CGFloat endAngle = 90.0 - 360.0 * progress_;
+ if (endAngle < 0.0)
+ endAngle += 360.0;
+ progressSlice = [NSBezierPath bezierPath];
+ [progressSlice moveToPoint:badgeCenter];
+ [progressSlice appendBezierPathWithArcWithCenter:badgeCenter
+ radius:badgeRadius
+ startAngle:90.0
+ endAngle:endAngle
+ clockwise:YES];
+ [progressSlice closePath];
+ }
+ [NSGraphicsContext saveGraphicsState];
+ [progressSlice addClip];
+ [sliceGradient drawFromCenter:badgeCenter
+ radius:0.0
+ toCenter:badgeCenter
+ radius:badgeRadius
+ options:0];
+ [NSGraphicsContext restoreGraphicsState];
+ }
+
+ // Edge
+ [NSGraphicsContext saveGraphicsState];
+ [[NSColor whiteColor] set];
+ scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
+ [shadow.get() setShadowOffset:NSMakeSize(0, -2)];
+ [shadow setShadowBlurRadius:2];
+ [shadow set];
+ [badgeEdge setLineWidth:2];
+ [badgeEdge stroke];
+ [NSGraphicsContext restoreGraphicsState];
+
+ // Download count
+ scoped_nsobject<NSNumberFormatter> formatter(
+ [[NSNumberFormatter alloc] init]);
+ NSString* countString =
+ [formatter stringFromNumber:[NSNumber numberWithInt:downloads_]];
+
+ scoped_nsobject<NSShadow> countShadow([[NSShadow alloc] init]);
+ [countShadow setShadowBlurRadius:3.0];
+ [countShadow.get() setShadowColor:[NSColor whiteColor]];
+ [countShadow.get() setShadowOffset:NSMakeSize(0.0, 0.0)];
+ NSMutableDictionary* countAttrsDict =
+ [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ [NSColor blackColor], NSForegroundColorAttributeName,
+ countShadow.get(), NSShadowAttributeName,
+ nil];
+ CGFloat countFontSize = badgeRadius;
+ NSSize countSize = NSZeroSize;
+ scoped_nsobject<NSAttributedString> countAttrString;
+ while (1) {
+ NSFont* countFont = [NSFont fontWithName:@"Helvetica-Bold"
+ size:countFontSize];
+ [countAttrsDict setObject:countFont forKey:NSFontAttributeName];
+ countAttrString.reset(
+ [[NSAttributedString alloc] initWithString:countString
+ attributes:countAttrsDict]);
+ countSize = [countAttrString size];
+ if (countSize.width > badgeRadius * 1.5) {
+ countFontSize -= 1.0;
+ } else {
+ break;
+ }
+ }
+
+ NSPoint countOrigin = badgeCenter;
+ countOrigin.x -= countSize.width / 2;
+ countOrigin.y -= countSize.height / 2.2; // tweak; otherwise too low
+
+ [countAttrString.get() drawAtPoint:countOrigin];
+}
+
+@end
+
+
+@implementation DockIcon
+
++ (DockIcon*)sharedDockIcon {
+ static DockIcon* icon;
+ if (!icon) {
+ NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile];
+
+ scoped_nsobject<DockTileView> dockTileView([[DockTileView alloc] init]);
+ [dockTile setContentView:dockTileView];
+
+ icon = [[DockIcon alloc] init];
+ }
+
+ return icon;
+}
+
+- (void)updateIcon {
+ NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile];
+
+ [dockTile display];
+}
+
+- (void)setDownloads:(int)downloads {
+ NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile];
+ DockTileView* dockTileView = (DockTileView*)([dockTile contentView]);
+
+ [dockTileView setDownloads:downloads];
+}
+
+- (void)setIndeterminate:(BOOL)indeterminate {
+ NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile];
+ DockTileView* dockTileView = (DockTileView*)([dockTile contentView]);
+
+ [dockTileView setIndeterminate:indeterminate];
+}
+
+- (void)setProgress:(float)progress {
+ NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile];
+ DockTileView* dockTileView = (DockTileView*)([dockTile contentView]);
+
+ [dockTileView setProgress:progress];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_item_button.h b/chrome/browser/ui/cocoa/download/download_item_button.h
new file mode 100644
index 0000000..c344341
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_button.h
@@ -0,0 +1,27 @@
+// 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 "base/cocoa_protocols_mac.h"
+#include "base/file_path.h"
+#import "chrome/browser/ui/cocoa/draggable_button.h"
+
+@class DownloadItemController;
+
+// A button that is a drag source for a file and that displays a context menu
+// instead of firing an action when clicked in a certain area.
+@interface DownloadItemButton : DraggableButton<NSMenuDelegate> {
+ @private
+ FilePath downloadPath_;
+ DownloadItemController* controller_; // weak
+}
+
+@property(assign, nonatomic) FilePath download;
+@property(assign, nonatomic) DownloadItemController* controller;
+
+// Overridden from DraggableButton.
+- (void)beginDrag:(NSEvent*)event;
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_item_button.mm b/chrome/browser/ui/cocoa/download/download_item_button.mm
new file mode 100644
index 0000000..da9f6b4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_button.mm
@@ -0,0 +1,50 @@
+// 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/ui/cocoa/download/download_item_button.h"
+
+#include "base/logging.h"
+#include "base/sys_string_conversions.h"
+#import "chrome/browser/ui/cocoa/download/download_item_cell.h"
+#import "chrome/browser/ui/cocoa/download/download_item_controller.h"
+
+@implementation DownloadItemButton
+
+@synthesize download = downloadPath_;
+@synthesize controller = controller_;
+
+// Overridden from DraggableButton.
+- (void)beginDrag:(NSEvent*)event {
+ if (!downloadPath_.empty()) {
+ NSString* filename = base::SysUTF8ToNSString(downloadPath_.value());
+ [self dragFile:filename fromRect:[self bounds] slideBack:YES event:event];
+ }
+}
+
+// Override to show a context menu on mouse down if clicked over the context
+// menu area.
+- (void)mouseDown:(NSEvent*)event {
+ DCHECK(controller_);
+ // Override so that we can pop up a context menu on mouse down.
+ NSCell* cell = [self cell];
+ DCHECK([cell respondsToSelector:@selector(isMouseOverButtonPart)]);
+ if ([reinterpret_cast<DownloadItemCell*>(cell) isMouseOverButtonPart]) {
+ [super mouseDown:event];
+ } else {
+ // Hold a reference to our controller in case the download completes and we
+ // represent a file that's auto-removed (e.g. a theme).
+ scoped_nsobject<DownloadItemController> ref([controller_ retain]);
+ [cell setHighlighted:YES];
+ [[self menu] setDelegate:self];
+ [NSMenu popUpContextMenu:[self menu]
+ withEvent:[NSApp currentEvent]
+ forView:self];
+ }
+}
+
+- (void)menuDidClose:(NSMenu*)menu {
+ [[self cell] setHighlighted:NO];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_item_button_unittest.mm b/chrome/browser/ui/cocoa/download/download_item_button_unittest.mm
new file mode 100644
index 0000000..bb0279d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_button_unittest.mm
@@ -0,0 +1,21 @@
+// 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/ui/cocoa/download/download_item_button.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+// Make sure nothing leaks.
+TEST(DownloadItemButtonTest, Create) {
+ scoped_nsobject<DownloadItemButton> button;
+ button.reset([[DownloadItemButton alloc]
+ initWithFrame:NSMakeRect(0,0,500,500)]);
+
+ // Test setter
+ FilePath path("foo");
+ [button.get() setDownload:path];
+ EXPECT_EQ(path.value(), [button.get() download].value());
+}
diff --git a/chrome/browser/ui/cocoa/download/download_item_cell.h b/chrome/browser/ui/cocoa/download/download_item_cell.h
new file mode 100644
index 0000000..4f24837
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_cell.h
@@ -0,0 +1,61 @@
+// 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_UI_COCOA_DOWNLOAD_DOWNLOAD_ITEM_CELL_H_
+#define CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_ITEM_CELL_H_
+#pragma once
+
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_ptr.h"
+#import "chrome/browser/ui/cocoa/gradient_button_cell.h"
+
+#include "base/file_path.h"
+
+class BaseDownloadItemModel;
+
+// A button cell that implements the weird button/popup button hybrid that is
+// used by the download items.
+
+// The button represented by this cell consists of a button part on the left
+// and a dropdown-menu part on the right. This enum describes which part the
+// mouse cursor is over currently.
+enum DownloadItemMousePosition {
+ kDownloadItemMouseOutside,
+ kDownloadItemMouseOverButtonPart,
+ kDownloadItemMouseOverDropdownPart
+};
+
+@interface DownloadItemCell : GradientButtonCell<NSAnimationDelegate> {
+ @private
+ // Track which part of the button the mouse is over
+ DownloadItemMousePosition mousePosition_;
+ int mouseInsideCount_;
+ scoped_nsobject<NSTrackingArea> trackingAreaButton_;
+ scoped_nsobject<NSTrackingArea> trackingAreaDropdown_;
+
+ FilePath downloadPath_; // stored unelided
+ NSString* secondaryTitle_;
+ NSFont* secondaryFont_;
+ int percentDone_;
+ scoped_nsobject<NSAnimation> completionAnimation_;
+
+ BOOL isStatusTextVisible_;
+ CGFloat titleY_;
+ CGFloat statusAlpha_;
+ scoped_nsobject<NSAnimation> hideStatusAnimation_;
+
+ scoped_ptr<ThemeProvider> themeProvider_;
+}
+
+- (void)setStateFromDownload:(BaseDownloadItemModel*)downloadModel;
+
+@property (nonatomic, copy) NSString* secondaryTitle;
+@property (nonatomic, retain) NSFont* secondaryFont;
+
+// Returns if the mouse is over the button part of the cell.
+- (BOOL)isMouseOverButtonPart;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_ITEM_CELL_H_
diff --git a/chrome/browser/ui/cocoa/download/download_item_cell.mm b/chrome/browser/ui/cocoa/download/download_item_cell.mm
new file mode 100644
index 0000000..b83d6f5
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_cell.mm
@@ -0,0 +1,708 @@
+// 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/ui/cocoa/download/download_item_cell.h"
+
+#include "app/l10n_util.h"
+#include "app/text_elider.h"
+#include "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/download/download_item.h"
+#include "chrome/browser/download/download_item_model.h"
+#include "chrome/browser/download/download_manager.h"
+#include "chrome/browser/download/download_util.h"
+#import "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/download/download_item_cell.h"
+#import "chrome/browser/ui/cocoa/image_utils.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#include "gfx/canvas_skia_paint.h"
+#include "grit/theme_resources.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+#import "third_party/GTM/AppKit/GTMNSColor+Luminance.h"
+
+namespace {
+
+// Distance from top border to icon
+const CGFloat kImagePaddingTop = 7;
+
+// Distance from left border to icon
+const CGFloat kImagePaddingLeft = 9;
+
+// Width of icon
+const CGFloat kImageWidth = 16;
+
+// Height of icon
+const CGFloat kImageHeight = 16;
+
+// x coordinate of download name string, in view coords
+const CGFloat kTextPosLeft = kImagePaddingLeft +
+ kImageWidth + download_util::kSmallProgressIconOffset;
+
+// Distance from end of download name string to dropdown area
+const CGFloat kTextPaddingRight = 3;
+
+// y coordinate of download name string, in view coords, when status message
+// is visible
+const CGFloat kPrimaryTextPosTop = 3;
+
+// y coordinate of download name string, in view coords, when status message
+// is not visible
+const CGFloat kPrimaryTextOnlyPosTop = 10;
+
+// y coordinate of status message, in view coords
+const CGFloat kSecondaryTextPosTop = 18;
+
+// Grey value of status text
+const CGFloat kSecondaryTextColor = 0.5;
+
+// Width of dropdown area on the right (includes 1px for the border on each
+// side).
+const CGFloat kDropdownAreaWidth = 14;
+
+// Width of dropdown arrow
+const CGFloat kDropdownArrowWidth = 5;
+
+// Height of dropdown arrow
+const CGFloat kDropdownArrowHeight = 3;
+
+// Vertical displacement of dropdown area, relative to the "centered" position.
+const CGFloat kDropdownAreaY = -2;
+
+// Duration of the two-lines-to-one-line animation, in seconds
+NSTimeInterval kHideStatusDuration = 0.3;
+
+// Duration of the 'download complete' animation, in seconds
+const int kCompleteAnimationDuration = 2.5;
+
+}
+
+// This is a helper class to animate the fading out of the status text.
+@interface DownloadItemCellAnimation : NSAnimation {
+ DownloadItemCell* cell_;
+}
+- (id)initWithDownloadItemCell:(DownloadItemCell*)cell
+ duration:(NSTimeInterval)duration
+ animationCurve:(NSAnimationCurve)animationCurve;
+@end
+
+class BackgroundTheme : public ThemeProvider {
+public:
+ BackgroundTheme(ThemeProvider* provider);
+
+ virtual void Init(Profile* profile) { }
+ virtual SkBitmap* GetBitmapNamed(int id) const { return nil; }
+ virtual SkColor GetColor(int id) const { return SkColor(); }
+ virtual bool GetDisplayProperty(int id, int* result) const { return false; }
+ virtual bool ShouldUseNativeFrame() const { return false; }
+ virtual bool HasCustomImage(int id) const { return false; }
+ virtual RefCountedMemory* GetRawData(int id) const { return NULL; }
+ virtual NSImage* GetNSImageNamed(int id, bool allow_default) const;
+ virtual NSColor* GetNSImageColorNamed(int id, bool allow_default) const;
+ virtual NSColor* GetNSColor(int id, bool allow_default) const;
+ virtual NSColor* GetNSColorTint(int id, bool allow_default) const;
+ virtual NSGradient* GetNSGradient(int id) const;
+
+private:
+ ThemeProvider* provider_;
+ scoped_nsobject<NSGradient> buttonGradient_;
+ scoped_nsobject<NSGradient> buttonPressedGradient_;
+ scoped_nsobject<NSColor> borderColor_;
+};
+
+BackgroundTheme::BackgroundTheme(ThemeProvider* provider) :
+ provider_(provider) {
+ NSColor* bgColor = [NSColor colorWithCalibratedRed:241/255.0
+ green:245/255.0
+ blue:250/255.0
+ alpha:77/255.0];
+ NSColor* clickedColor = [NSColor colorWithCalibratedRed:239/255.0
+ green:245/255.0
+ blue:252/255.0
+ alpha:51/255.0];
+
+ borderColor_.reset(
+ [[NSColor colorWithCalibratedWhite:0 alpha:36/255.0] retain]);
+ buttonGradient_.reset([[NSGradient alloc]
+ initWithColors:[NSArray arrayWithObject:bgColor]]);
+ buttonPressedGradient_.reset([[NSGradient alloc]
+ initWithColors:[NSArray arrayWithObject:clickedColor]]);
+}
+
+NSImage* BackgroundTheme::GetNSImageNamed(int id, bool allow_default) const {
+ return nil;
+}
+
+NSColor* BackgroundTheme::GetNSImageColorNamed(int id,
+ bool allow_default) const {
+ return nil;
+}
+
+NSColor* BackgroundTheme::GetNSColor(int id, bool allow_default) const {
+ return provider_->GetNSColor(id, allow_default);
+}
+
+NSColor* BackgroundTheme::GetNSColorTint(int id, bool allow_default) const {
+ if (id == BrowserThemeProvider::TINT_BUTTONS)
+ return borderColor_.get();
+
+ return provider_->GetNSColorTint(id, allow_default);
+}
+
+NSGradient* BackgroundTheme::GetNSGradient(int id) const {
+ switch (id) {
+ case BrowserThemeProvider::GRADIENT_TOOLBAR_BUTTON:
+ case BrowserThemeProvider::GRADIENT_TOOLBAR_BUTTON_INACTIVE:
+ return buttonGradient_.get();
+ case BrowserThemeProvider::GRADIENT_TOOLBAR_BUTTON_PRESSED:
+ case BrowserThemeProvider::GRADIENT_TOOLBAR_BUTTON_PRESSED_INACTIVE:
+ return buttonPressedGradient_.get();
+ default:
+ return provider_->GetNSGradient(id);
+ }
+}
+
+@interface DownloadItemCell(Private)
+- (void)updateTrackingAreas:(id)sender;
+- (void)hideSecondaryTitle;
+- (void)animation:(NSAnimation*)animation
+ progressed:(NSAnimationProgress)progress;
+- (NSString*)elideTitle:(int)availableWidth;
+- (NSString*)elideStatus:(int)availableWidth;
+- (ThemeProvider*)backgroundThemeWrappingProvider:(ThemeProvider*)provider;
+- (BOOL)pressedWithDefaultThemeOnPart:(DownloadItemMousePosition)part;
+- (NSColor*)titleColorForPart:(DownloadItemMousePosition)part;
+- (void)drawSecondaryTitleInRect:(NSRect)innerFrame;
+@end
+
+@implementation DownloadItemCell
+
+@synthesize secondaryTitle = secondaryTitle_;
+@synthesize secondaryFont = secondaryFont_;
+
+- (void)setInitialState {
+ isStatusTextVisible_ = NO;
+ titleY_ = kPrimaryTextPosTop;
+ statusAlpha_ = 1.0;
+
+ [self setFont:[NSFont systemFontOfSize:
+ [NSFont systemFontSizeForControlSize:NSSmallControlSize]]];
+ [self setSecondaryFont:[NSFont systemFontOfSize:
+ [NSFont systemFontSizeForControlSize:NSSmallControlSize]]];
+
+ [self updateTrackingAreas:self];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(updateTrackingAreas:)
+ name:NSViewFrameDidChangeNotification
+ object:[self controlView]];
+}
+
+// For nib instantiations
+- (id)initWithCoder:(NSCoder*)decoder {
+ if ((self = [super initWithCoder:decoder])) {
+ [self setInitialState];
+ }
+ return self;
+}
+
+// For programmatic instantiations.
+- (id)initTextCell:(NSString *)string {
+ if ((self = [super initTextCell:string])) {
+ [self setInitialState];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ if ([completionAnimation_ isAnimating])
+ [completionAnimation_ stopAnimation];
+ if ([hideStatusAnimation_ isAnimating])
+ [hideStatusAnimation_ stopAnimation];
+ if (trackingAreaButton_) {
+ [[self controlView] removeTrackingArea:trackingAreaButton_];
+ trackingAreaButton_.reset();
+ }
+ if (trackingAreaDropdown_) {
+ [[self controlView] removeTrackingArea:trackingAreaDropdown_];
+ trackingAreaDropdown_.reset();
+ }
+ [secondaryTitle_ release];
+ [secondaryFont_ release];
+ [super dealloc];
+}
+
+- (void)setStateFromDownload:(BaseDownloadItemModel*)downloadModel {
+ // Set the name of the download.
+ downloadPath_ = downloadModel->download()->GetFileNameToReportUser();
+
+ std::wstring statusText = downloadModel->GetStatusText();
+ if (statusText.empty()) {
+ // Remove the status text label.
+ [self hideSecondaryTitle];
+ isStatusTextVisible_ = NO;
+ } else {
+ // Set status text.
+ NSString* statusString = base::SysWideToNSString(statusText);
+ [self setSecondaryTitle:statusString];
+ isStatusTextVisible_ = YES;
+ }
+
+ switch (downloadModel->download()->state()) {
+ case DownloadItem::COMPLETE:
+ // Small downloads may start in a complete state due to asynchronous
+ // notifications. In this case, we'll get a second complete notification
+ // via the observers, so we ignore it and avoid creating a second complete
+ // animation.
+ if (completionAnimation_.get())
+ break;
+ completionAnimation_.reset([[DownloadItemCellAnimation alloc]
+ initWithDownloadItemCell:self
+ duration:kCompleteAnimationDuration
+ animationCurve:NSAnimationLinear]);
+ [completionAnimation_.get() setDelegate:self];
+ [completionAnimation_.get() startAnimation];
+ percentDone_ = -1;
+ break;
+ case DownloadItem::CANCELLED:
+ percentDone_ = -1;
+ break;
+ case DownloadItem::IN_PROGRESS:
+ percentDone_ = downloadModel->download()->is_paused() ?
+ -1 : downloadModel->download()->PercentComplete();
+ break;
+ default:
+ NOTREACHED();
+ }
+
+ [[self controlView] setNeedsDisplay:YES];
+}
+
+- (void)updateTrackingAreas:(id)sender {
+ if (trackingAreaButton_) {
+ [[self controlView] removeTrackingArea:trackingAreaButton_.get()];
+ trackingAreaButton_.reset(nil);
+ }
+ if (trackingAreaDropdown_) {
+ [[self controlView] removeTrackingArea:trackingAreaDropdown_.get()];
+ trackingAreaDropdown_.reset(nil);
+ }
+
+ // Use two distinct tracking rects for left and right parts.
+ // The tracking areas are also used to decide how to handle clicks. They must
+ // always be active, so the click is handled correctly when a download item
+ // is clicked while chrome is not the active app ( http://crbug.com/21916 ).
+ NSRect bounds = [[self controlView] bounds];
+ NSRect buttonRect, dropdownRect;
+ NSDivideRect(bounds, &dropdownRect, &buttonRect,
+ kDropdownAreaWidth, NSMaxXEdge);
+
+ trackingAreaButton_.reset([[NSTrackingArea alloc]
+ initWithRect:buttonRect
+ options:(NSTrackingMouseEnteredAndExited |
+ NSTrackingActiveAlways)
+ owner:self
+ userInfo:nil]);
+ [[self controlView] addTrackingArea:trackingAreaButton_.get()];
+
+ trackingAreaDropdown_.reset([[NSTrackingArea alloc]
+ initWithRect:dropdownRect
+ options:(NSTrackingMouseEnteredAndExited |
+ NSTrackingActiveAlways)
+ owner:self
+ userInfo:nil]);
+ [[self controlView] addTrackingArea:trackingAreaDropdown_.get()];
+}
+
+- (void)setShowsBorderOnlyWhileMouseInside:(BOOL)showOnly {
+ // Override to make sure it doesn't do anything if it's called accidentally.
+}
+
+- (void)mouseEntered:(NSEvent*)theEvent {
+ mouseInsideCount_++;
+ if ([theEvent trackingArea] == trackingAreaButton_.get())
+ mousePosition_ = kDownloadItemMouseOverButtonPart;
+ else if ([theEvent trackingArea] == trackingAreaDropdown_.get())
+ mousePosition_ = kDownloadItemMouseOverDropdownPart;
+ [[self controlView] setNeedsDisplay:YES];
+}
+
+- (void)mouseExited:(NSEvent *)theEvent {
+ mouseInsideCount_--;
+ if (mouseInsideCount_ == 0)
+ mousePosition_ = kDownloadItemMouseOutside;
+ [[self controlView] setNeedsDisplay:YES];
+}
+
+- (BOOL)isMouseInside {
+ return mousePosition_ != kDownloadItemMouseOutside;
+}
+
+- (BOOL)isMouseOverButtonPart {
+ return mousePosition_ == kDownloadItemMouseOverButtonPart;
+}
+
+- (BOOL)isButtonPartPressed {
+ return [self isHighlighted]
+ && mousePosition_ == kDownloadItemMouseOverButtonPart;
+}
+
+- (BOOL)isMouseOverDropdownPart {
+ return mousePosition_ == kDownloadItemMouseOverDropdownPart;
+}
+
+- (BOOL)isDropdownPartPressed {
+ return [self isHighlighted]
+ && mousePosition_ == kDownloadItemMouseOverDropdownPart;
+}
+
+- (NSBezierPath*)leftRoundedPath:(CGFloat)radius inRect:(NSRect)rect {
+
+ NSPoint topLeft = NSMakePoint(NSMinX(rect), NSMaxY(rect));
+ NSPoint topRight = NSMakePoint(NSMaxX(rect), NSMaxY(rect));
+ NSPoint bottomRight = NSMakePoint(NSMaxX(rect) , NSMinY(rect));
+
+ NSBezierPath* path = [NSBezierPath bezierPath];
+ [path moveToPoint:topRight];
+ [path appendBezierPathWithArcFromPoint:topLeft
+ toPoint:rect.origin
+ radius:radius];
+ [path appendBezierPathWithArcFromPoint:rect.origin
+ toPoint:bottomRight
+ radius:radius];
+ [path lineToPoint:bottomRight];
+ return path;
+}
+
+- (NSBezierPath*)rightRoundedPath:(CGFloat)radius inRect:(NSRect)rect {
+
+ NSPoint topLeft = NSMakePoint(NSMinX(rect), NSMaxY(rect));
+ NSPoint topRight = NSMakePoint(NSMaxX(rect), NSMaxY(rect));
+ NSPoint bottomRight = NSMakePoint(NSMaxX(rect), NSMinY(rect));
+
+ NSBezierPath* path = [NSBezierPath bezierPath];
+ [path moveToPoint:rect.origin];
+ [path appendBezierPathWithArcFromPoint:bottomRight
+ toPoint:topRight
+ radius:radius];
+ [path appendBezierPathWithArcFromPoint:topRight
+ toPoint:topLeft
+ radius:radius];
+ [path lineToPoint:topLeft];
+ return path;
+}
+
+- (NSString*)elideTitle:(int)availableWidth {
+ NSFont* font = [self font];
+ gfx::Font font_chr(base::SysNSStringToWide([font fontName]),
+ [font pointSize]);
+
+ return base::SysUTF16ToNSString(
+ ElideFilename(downloadPath_, font_chr, availableWidth));
+}
+
+- (NSString*)elideStatus:(int)availableWidth {
+ NSFont* font = [self secondaryFont];
+ gfx::Font font_chr(base::SysNSStringToWide([font fontName]),
+ [font pointSize]);
+
+ return base::SysUTF16ToNSString(ElideText(
+ base::SysNSStringToUTF16([self secondaryTitle]),
+ font_chr,
+ availableWidth,
+ false));
+}
+
+- (ThemeProvider*)backgroundThemeWrappingProvider:(ThemeProvider*)provider {
+ if (!themeProvider_.get()) {
+ themeProvider_.reset(new BackgroundTheme(provider));
+ }
+
+ return themeProvider_.get();
+}
+
+// Returns if |part| was pressed while the default theme was active.
+- (BOOL)pressedWithDefaultThemeOnPart:(DownloadItemMousePosition)part {
+ ThemeProvider* themeProvider = [[[self controlView] window] themeProvider];
+ bool isDefaultTheme =
+ !themeProvider->HasCustomImage(IDR_THEME_BUTTON_BACKGROUND);
+ return isDefaultTheme && [self isHighlighted] && mousePosition_ == part;
+}
+
+// Returns the text color that should be used to draw text on |part|.
+- (NSColor*)titleColorForPart:(DownloadItemMousePosition)part {
+ ThemeProvider* themeProvider = [[[self controlView] window] themeProvider];
+ NSColor* themeTextColor =
+ themeProvider->GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT,
+ true);
+ return [self pressedWithDefaultThemeOnPart:part]
+ ? [NSColor alternateSelectedControlTextColor] : themeTextColor;
+}
+
+- (void)drawSecondaryTitleInRect:(NSRect)innerFrame {
+ if (![self secondaryTitle] || statusAlpha_ <= 0)
+ return;
+
+ CGFloat textWidth = innerFrame.size.width -
+ (kTextPosLeft + kTextPaddingRight + kDropdownAreaWidth);
+ NSString* secondaryText = [self elideStatus:textWidth];
+ NSColor* secondaryColor =
+ [self titleColorForPart:kDownloadItemMouseOverButtonPart];
+
+ // If text is light-on-dark, lightening it alone will do nothing.
+ // Therefore we mute luminance a wee bit before drawing in this case.
+ if (![secondaryColor gtm_isDarkColor])
+ secondaryColor = [secondaryColor gtm_colorByAdjustingLuminance:-0.2];
+
+ NSDictionary* secondaryTextAttributes =
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ secondaryColor, NSForegroundColorAttributeName,
+ [self secondaryFont], NSFontAttributeName,
+ nil];
+ NSPoint secondaryPos =
+ NSMakePoint(innerFrame.origin.x + kTextPosLeft, kSecondaryTextPosTop);
+ [secondaryText drawAtPoint:secondaryPos
+ withAttributes:secondaryTextAttributes];
+}
+
+- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
+ // Constants from Cole. Will kConstant them once the feedback loop
+ // is complete.
+ NSRect drawFrame = NSInsetRect(cellFrame, 1.5, 1.5);
+ NSRect innerFrame = NSInsetRect(cellFrame, 2, 2);
+
+ const float radius = 5;
+ NSWindow* window = [controlView window];
+ BOOL active = [window isKeyWindow] || [window isMainWindow];
+
+ // In the default theme, draw download items with the bookmark button
+ // gradient. For some themes, this leads to unreadable text, so draw the item
+ // with a background that looks like windows (some transparent white) if a
+ // theme is used. Use custom theme object with a white color gradient to trick
+ // the superclass into drawing what we want.
+ ThemeProvider* themeProvider = [[[self controlView] window] themeProvider];
+ bool isDefaultTheme =
+ !themeProvider->HasCustomImage(IDR_THEME_BUTTON_BACKGROUND);
+
+ NSGradient* bgGradient = nil;
+ if (!isDefaultTheme) {
+ themeProvider = [self backgroundThemeWrappingProvider:themeProvider];
+ bgGradient = themeProvider->GetNSGradient(
+ active ? BrowserThemeProvider::GRADIENT_TOOLBAR_BUTTON :
+ BrowserThemeProvider::GRADIENT_TOOLBAR_BUTTON_INACTIVE);
+ }
+
+ NSRect buttonDrawRect, dropdownDrawRect;
+ NSDivideRect(drawFrame, &dropdownDrawRect, &buttonDrawRect,
+ kDropdownAreaWidth, NSMaxXEdge);
+
+ NSBezierPath* buttonInnerPath = [self
+ leftRoundedPath:radius inRect:buttonDrawRect];
+ NSBezierPath* dropdownInnerPath = [self
+ rightRoundedPath:radius inRect:dropdownDrawRect];
+
+ // Draw secondary title, if any. Do this before drawing the (transparent)
+ // fill so that the text becomes a bit lighter. The default theme's "pressed"
+ // gradient is not transparent, so only do this if a theme is active.
+ bool drawStatusOnTop =
+ [self pressedWithDefaultThemeOnPart:kDownloadItemMouseOverButtonPart];
+ if (!drawStatusOnTop)
+ [self drawSecondaryTitleInRect:innerFrame];
+
+ // Stroke the borders and appropriate fill gradient.
+ [self drawBorderAndFillForTheme:themeProvider
+ controlView:controlView
+ innerPath:buttonInnerPath
+ showClickedGradient:[self isButtonPartPressed]
+ showHighlightGradient:[self isMouseOverButtonPart]
+ hoverAlpha:0.0
+ active:active
+ cellFrame:cellFrame
+ defaultGradient:bgGradient];
+
+ [self drawBorderAndFillForTheme:themeProvider
+ controlView:controlView
+ innerPath:dropdownInnerPath
+ showClickedGradient:[self isDropdownPartPressed]
+ showHighlightGradient:[self isMouseOverDropdownPart]
+ hoverAlpha:0.0
+ active:active
+ cellFrame:cellFrame
+ defaultGradient:bgGradient];
+
+ [self drawInteriorWithFrame:innerFrame inView:controlView];
+
+ // For the default theme, draw the status text on top of the (opaque) button
+ // gradient.
+ if (drawStatusOnTop)
+ [self drawSecondaryTitleInRect:innerFrame];
+}
+
+- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
+ // Draw title
+ CGFloat textWidth = cellFrame.size.width -
+ (kTextPosLeft + kTextPaddingRight + kDropdownAreaWidth);
+ [self setTitle:[self elideTitle:textWidth]];
+
+ NSColor* color = [self titleColorForPart:kDownloadItemMouseOverButtonPart];
+ NSString* primaryText = [self title];
+
+ NSDictionary* primaryTextAttributes =
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ color, NSForegroundColorAttributeName,
+ [self font], NSFontAttributeName,
+ nil];
+ NSPoint primaryPos = NSMakePoint(
+ cellFrame.origin.x + kTextPosLeft,
+ titleY_);
+
+ [primaryText drawAtPoint:primaryPos withAttributes:primaryTextAttributes];
+
+ // Draw progress disk
+ {
+ // CanvasSkiaPaint draws its content to the current NSGraphicsContext in its
+ // destructor, which needs to be invoked before the icon is drawn below -
+ // hence this nested block.
+
+ // Always repaint the whole disk.
+ NSPoint imagePosition = [self imageRectForBounds:cellFrame].origin;
+ int x = imagePosition.x - download_util::kSmallProgressIconOffset;
+ int y = imagePosition.y - download_util::kSmallProgressIconOffset;
+ NSRect dirtyRect = NSMakeRect(
+ x, y,
+ download_util::kSmallProgressIconSize,
+ download_util::kSmallProgressIconSize);
+
+ gfx::CanvasSkiaPaint canvas(dirtyRect, false);
+ canvas.set_composite_alpha(true);
+ if (completionAnimation_.get()) {
+ if ([completionAnimation_ isAnimating]) {
+ download_util::PaintDownloadComplete(&canvas,
+ x, y,
+ [completionAnimation_ currentValue],
+ download_util::SMALL);
+ }
+ } else if (percentDone_ >= 0) {
+ download_util::PaintDownloadProgress(&canvas,
+ x, y,
+ download_util::kStartAngleDegrees, // TODO(thakis): Animate
+ percentDone_,
+ download_util::SMALL);
+ }
+ }
+
+ // Draw icon
+ NSRect imageRect = NSZeroRect;
+ imageRect.size = [[self image] size];
+ [[self image] drawInRect:[self imageRectForBounds:cellFrame]
+ fromRect:imageRect
+ operation:NSCompositeSourceOver
+ fraction:[self isEnabled] ? 1.0 : 0.5
+ neverFlipped:YES];
+
+ // Separator between button and popup parts
+ CGFloat lx = NSMaxX(cellFrame) - kDropdownAreaWidth + 0.5;
+ [[NSColor colorWithDeviceWhite:0.0 alpha:0.1] set];
+ [NSBezierPath strokeLineFromPoint:NSMakePoint(lx, NSMinY(cellFrame) + 1)
+ toPoint:NSMakePoint(lx, NSMaxY(cellFrame) - 1)];
+ [[NSColor colorWithDeviceWhite:1.0 alpha:0.1] set];
+ [NSBezierPath strokeLineFromPoint:NSMakePoint(lx + 1, NSMinY(cellFrame) + 1)
+ toPoint:NSMakePoint(lx + 1, NSMaxY(cellFrame) - 1)];
+
+ // Popup arrow. Put center of mass of the arrow in the center of the
+ // dropdown area.
+ CGFloat cx = NSMaxX(cellFrame) - kDropdownAreaWidth/2 + 0.5;
+ CGFloat cy = NSMidY(cellFrame);
+ NSPoint p1 = NSMakePoint(cx - kDropdownArrowWidth/2,
+ cy - kDropdownArrowHeight/3 + kDropdownAreaY);
+ NSPoint p2 = NSMakePoint(cx + kDropdownArrowWidth/2,
+ cy - kDropdownArrowHeight/3 + kDropdownAreaY);
+ NSPoint p3 = NSMakePoint(cx, cy + kDropdownArrowHeight*2/3 + kDropdownAreaY);
+ NSBezierPath *triangle = [NSBezierPath bezierPath];
+ [triangle moveToPoint:p1];
+ [triangle lineToPoint:p2];
+ [triangle lineToPoint:p3];
+ [triangle closePath];
+
+ NSGraphicsContext* context = [NSGraphicsContext currentContext];
+ [context saveGraphicsState];
+
+ scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
+ [shadow.get() setShadowColor:[NSColor whiteColor]];
+ [shadow.get() setShadowOffset:NSMakeSize(0, -1)];
+ [shadow setShadowBlurRadius:0.0];
+ [shadow set];
+
+ NSColor* fill = [self titleColorForPart:kDownloadItemMouseOverDropdownPart];
+ [fill setFill];
+
+ [triangle fill];
+
+ [context restoreGraphicsState];
+}
+
+- (NSRect)imageRectForBounds:(NSRect)cellFrame {
+ return NSMakeRect(cellFrame.origin.x + kImagePaddingLeft,
+ cellFrame.origin.y + kImagePaddingTop,
+ kImageWidth,
+ kImageHeight);
+}
+
+- (void)hideSecondaryTitle {
+ if (isStatusTextVisible_) {
+ // No core animation -- text in CA layers is not subpixel antialiased :-/
+ hideStatusAnimation_.reset([[DownloadItemCellAnimation alloc]
+ initWithDownloadItemCell:self
+ duration:kHideStatusDuration
+ animationCurve:NSAnimationEaseIn]);
+ [hideStatusAnimation_.get() setDelegate:self];
+ [hideStatusAnimation_.get() startAnimation];
+ } else {
+ // If the download is done so quickly that the status line is never visible,
+ // don't show an animation
+ [self animation:nil progressed:1.0];
+ }
+}
+
+- (void)animation:(NSAnimation*)animation
+ progressed:(NSAnimationProgress)progress {
+ if (animation == hideStatusAnimation_ || animation == nil) {
+ titleY_ = progress*kPrimaryTextOnlyPosTop +
+ (1 - progress)*kPrimaryTextPosTop;
+ statusAlpha_ = 1 - progress;
+ [[self controlView] setNeedsDisplay:YES];
+ } else if (animation == completionAnimation_) {
+ [[self controlView] setNeedsDisplay:YES];
+ }
+}
+
+- (void)animationDidEnd:(NSAnimation *)animation {
+ if (animation == hideStatusAnimation_)
+ hideStatusAnimation_.reset();
+ else if (animation == completionAnimation_)
+ completionAnimation_.reset();
+}
+
+@end
+
+@implementation DownloadItemCellAnimation
+
+- (id)initWithDownloadItemCell:(DownloadItemCell*)cell
+ duration:(NSTimeInterval)duration
+ animationCurve:(NSAnimationCurve)animationCurve {
+ if ((self = [super gtm_initWithDuration:duration
+ eventMask:NSLeftMouseDownMask
+ animationCurve:animationCurve])) {
+ cell_ = cell;
+ [self setAnimationBlockingMode:NSAnimationNonblocking];
+ }
+ return self;
+}
+
+- (void)setCurrentProgress:(NSAnimationProgress)progress {
+ [super setCurrentProgress:progress];
+ [cell_ animation:self progressed:progress];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_item_controller.h b/chrome/browser/ui/cocoa/download/download_item_controller.h
new file mode 100644
index 0000000..dea7722
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_controller.h
@@ -0,0 +1,105 @@
+// 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_ptr.h"
+#include "base/time.h"
+
+class BaseDownloadItemModel;
+@class ChromeUILocalizer;
+@class DownloadItemCell;
+class DownloadItem;
+@class DownloadItemButton;
+class DownloadItemMac;
+class DownloadShelfContextMenuMac;
+@class DownloadShelfController;
+@class GTMWidthBasedTweaker;
+
+// A controller class that manages one download item.
+
+@interface DownloadItemController : NSViewController {
+ @private
+ IBOutlet DownloadItemButton* progressView_;
+ IBOutlet DownloadItemCell* cell_;
+
+ IBOutlet NSMenu* activeDownloadMenu_;
+ IBOutlet NSMenu* completeDownloadMenu_;
+
+ // This is shown instead of progressView_ for dangerous downloads.
+ IBOutlet NSView* dangerousDownloadView_;
+ IBOutlet NSTextField* dangerousDownloadLabel_;
+ IBOutlet NSButton* dangerousDownloadConfirmButton_;
+
+ // Needed to find out how much the tweaker changed sizes to update the
+ // other views.
+ IBOutlet GTMWidthBasedTweaker* buttonTweaker_;
+
+ // Because the confirm text and button for dangerous downloads are determined
+ // at runtime, an outlet to the localizer is needed to construct the layout
+ // tweaker in awakeFromNib in order to adjust the UI after all strings are
+ // determined.
+ IBOutlet ChromeUILocalizer* localizer_;
+
+ IBOutlet NSImageView* image_;
+
+ scoped_ptr<DownloadItemMac> bridge_;
+ scoped_ptr<DownloadShelfContextMenuMac> menuBridge_;
+
+ // Weak pointer to the shelf that owns us.
+ DownloadShelfController* shelf_;
+
+ // The time at which this view was created.
+ base::Time creationTime_;
+
+ // The state of this item.
+ enum DownoadItemState {
+ kNormal,
+ kDangerous
+ } state_;
+};
+
+// Takes ownership of |downloadModel|.
+- (id)initWithModel:(BaseDownloadItemModel*)downloadModel
+ shelf:(DownloadShelfController*)shelf;
+
+// Updates the UI and menu state from |downloadModel|.
+- (void)setStateFromDownload:(BaseDownloadItemModel*)downloadModel;
+
+// Remove ourself from the download UI.
+- (void)remove;
+
+// Update item's visibility depending on if the item is still completely
+// contained in its parent.
+- (void)updateVisibility:(id)sender;
+
+// Asynchronous icon loading callback.
+- (void)setIcon:(NSImage*)icon;
+
+// Download item button clicked
+- (IBAction)handleButtonClick:(id)sender;
+
+// Returns the size this item wants to have.
+- (NSSize)preferredSize;
+
+// Returns the DownloadItem model object belonging to this item.
+- (DownloadItem*)download;
+
+// Updates the tooltip with the download's path.
+- (void)updateToolTip;
+
+// Handling of dangerous downloads
+- (void)clearDangerousMode;
+- (BOOL)isDangerousMode;
+- (IBAction)saveDownload:(id)sender;
+- (IBAction)discardDownload:(id)sender;
+
+// Context menu handlers.
+- (IBAction)handleOpen:(id)sender;
+- (IBAction)handleAlwaysOpen:(id)sender;
+- (IBAction)handleReveal:(id)sender;
+- (IBAction)handleCancel:(id)sender;
+- (IBAction)handleTogglePause:(id)sender;
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_item_controller.mm b/chrome/browser/ui/cocoa/download/download_item_controller.mm
new file mode 100644
index 0000000..0d67b83
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_controller.mm
@@ -0,0 +1,398 @@
+// 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/ui/cocoa/download/download_item_controller.h"
+
+#include "app/l10n_util_mac.h"
+#include "app/resource_bundle.h"
+#include "app/text_elider.h"
+#include "base/mac_util.h"
+#include "base/metrics/histogram.h"
+#include "base/string16.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/download/download_item.h"
+#include "chrome/browser/download/download_item_model.h"
+#include "chrome/browser/download/download_shelf.h"
+#include "chrome/browser/download/download_util.h"
+#import "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/download/download_item_button.h"
+#import "chrome/browser/ui/cocoa/download/download_item_cell.h"
+#include "chrome/browser/ui/cocoa/download/download_item_mac.h"
+#import "chrome/browser/ui/cocoa/download/download_shelf_controller.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#import "chrome/browser/ui/cocoa/ui_localizer.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+namespace {
+
+// NOTE: Mac currently doesn't use this like Windows does. Mac uses this to
+// control the min size on the dangerous download text. TVL sent a query off to
+// UX to fully spec all the the behaviors of download items and truncations
+// rules so all platforms can get inline in the future.
+const int kTextWidth = 140; // Pixels
+
+// The maximum number of characters we show in a file name when displaying the
+// dangerous download message.
+const int kFileNameMaxLength = 20;
+
+// The maximum width in pixels for the file name tooltip.
+const int kToolTipMaxWidth = 900;
+
+
+// Helper to widen a view.
+void WidenView(NSView* view, CGFloat widthChange) {
+ // If it is an NSBox, the autoresize of the contentView is the issue.
+ NSView* contentView = view;
+ if ([view isKindOfClass:[NSBox class]]) {
+ contentView = [(NSBox*)view contentView];
+ }
+ BOOL autoresizesSubviews = [contentView autoresizesSubviews];
+ if (autoresizesSubviews) {
+ [contentView setAutoresizesSubviews:NO];
+ }
+
+ NSRect frame = [view frame];
+ frame.size.width += widthChange;
+ [view setFrame:frame];
+
+ if (autoresizesSubviews) {
+ [contentView setAutoresizesSubviews:YES];
+ }
+}
+
+} // namespace
+
+// A class for the chromium-side part of the download shelf context menu.
+
+class DownloadShelfContextMenuMac : public DownloadShelfContextMenu {
+ public:
+ DownloadShelfContextMenuMac(BaseDownloadItemModel* model)
+ : DownloadShelfContextMenu(model) { }
+
+ using DownloadShelfContextMenu::ExecuteCommand;
+ using DownloadShelfContextMenu::IsCommandIdChecked;
+ using DownloadShelfContextMenu::IsCommandIdEnabled;
+
+ using DownloadShelfContextMenu::SHOW_IN_FOLDER;
+ using DownloadShelfContextMenu::OPEN_WHEN_COMPLETE;
+ using DownloadShelfContextMenu::ALWAYS_OPEN_TYPE;
+ using DownloadShelfContextMenu::CANCEL;
+ using DownloadShelfContextMenu::TOGGLE_PAUSE;
+};
+
+@interface DownloadItemController (Private)
+- (void)themeDidChangeNotification:(NSNotification*)aNotification;
+- (void)updateTheme:(ThemeProvider*)themeProvider;
+- (void)setState:(DownoadItemState)state;
+@end
+
+// Implementation of DownloadItemController
+
+@implementation DownloadItemController
+
+- (id)initWithModel:(BaseDownloadItemModel*)downloadModel
+ shelf:(DownloadShelfController*)shelf {
+ if ((self = [super initWithNibName:@"DownloadItem"
+ bundle:mac_util::MainAppBundle()])) {
+ // Must be called before [self view], so that bridge_ is set in awakeFromNib
+ bridge_.reset(new DownloadItemMac(downloadModel, self));
+ menuBridge_.reset(new DownloadShelfContextMenuMac(downloadModel));
+
+ NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
+ [defaultCenter addObserver:self
+ selector:@selector(themeDidChangeNotification:)
+ name:kBrowserThemeDidChangeNotification
+ object:nil];
+
+ shelf_ = shelf;
+ state_ = kNormal;
+ creationTime_ = base::Time::Now();
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [progressView_ setController:nil];
+ [[self view] removeFromSuperview];
+ [super dealloc];
+}
+
+- (void)awakeFromNib {
+ [progressView_ setController:self];
+
+ [self setStateFromDownload:bridge_->download_model()];
+
+ GTMUILocalizerAndLayoutTweaker* localizerAndLayoutTweaker =
+ [[[GTMUILocalizerAndLayoutTweaker alloc] init] autorelease];
+ [localizerAndLayoutTweaker applyLocalizer:localizer_ tweakingUI:[self view]];
+
+ // The strings are based on the download item's name, sizing tweaks have to be
+ // manually done.
+ DCHECK(buttonTweaker_ != nil);
+ CGFloat widthChange = [buttonTweaker_ changedWidth];
+ // If it's a dangerous download, size the two lines so the text/filename
+ // is always visible.
+ if ([self isDangerousMode]) {
+ widthChange +=
+ [GTMUILocalizerAndLayoutTweaker
+ sizeToFitFixedHeightTextField:dangerousDownloadLabel_
+ minWidth:kTextWidth];
+ }
+ // Grow the parent views
+ WidenView([self view], widthChange);
+ WidenView(dangerousDownloadView_, widthChange);
+ // Slide the two buttons over.
+ NSPoint frameOrigin = [buttonTweaker_ frame].origin;
+ frameOrigin.x += widthChange;
+ [buttonTweaker_ setFrameOrigin:frameOrigin];
+
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ NSImage* alertIcon = rb.GetNativeImageNamed(IDR_WARNING);
+ DCHECK(alertIcon);
+ [image_ setImage:alertIcon];
+
+ bridge_->LoadIcon();
+ [self updateToolTip];
+}
+
+- (void)setStateFromDownload:(BaseDownloadItemModel*)downloadModel {
+ DCHECK_EQ(bridge_->download_model(), downloadModel);
+
+ // Handle dangerous downloads.
+ if (downloadModel->download()->safety_state() == DownloadItem::DANGEROUS) {
+ [self setState:kDangerous];
+
+ NSString* dangerousWarning;
+ NSString* confirmButtonTitle;
+ // The dangerous download label and button text are different for an
+ // extension file.
+ if (downloadModel->download()->is_extension_install()) {
+ dangerousWarning = l10n_util::GetNSStringWithFixup(
+ IDS_PROMPT_DANGEROUS_DOWNLOAD_EXTENSION);
+ confirmButtonTitle = l10n_util::GetNSStringWithFixup(
+ IDS_CONTINUE_EXTENSION_DOWNLOAD);
+ } else {
+ // This basic fixup copies Windows DownloadItemView::DownloadItemView().
+
+ // Extract the file extension (if any).
+ FilePath filename(downloadModel->download()->target_name());
+ FilePath::StringType extension = filename.Extension();
+
+ // Remove leading '.' from the extension
+ if (extension.length() > 0)
+ extension = extension.substr(1);
+
+ // Elide giant extensions.
+ if (extension.length() > kFileNameMaxLength / 2) {
+ std::wstring wide_extension;
+ ElideString(UTF8ToWide(extension), kFileNameMaxLength / 2,
+ &wide_extension);
+ extension = WideToUTF8(wide_extension);
+ }
+
+ // Rebuild the filename.extension.
+ std::wstring rootname = UTF8ToWide(filename.RemoveExtension().value());
+ ElideString(rootname, kFileNameMaxLength - extension.length(), &rootname);
+ std::string new_filename = WideToUTF8(rootname);
+ if (extension.length())
+ new_filename += std::string(".") + extension;
+
+ dangerousWarning = l10n_util::GetNSStringFWithFixup(
+ IDS_PROMPT_DANGEROUS_DOWNLOAD, UTF8ToUTF16(new_filename));
+ confirmButtonTitle = l10n_util::GetNSStringWithFixup(IDS_SAVE_DOWNLOAD);
+ }
+ [dangerousDownloadLabel_ setStringValue:dangerousWarning];
+ [dangerousDownloadConfirmButton_ setTitle:confirmButtonTitle];
+ return;
+ }
+
+ // Set correct popup menu. Also, set draggable download on completion.
+ if (downloadModel->download()->state() == DownloadItem::COMPLETE) {
+ [progressView_ setMenu:completeDownloadMenu_];
+ [progressView_ setDownload:downloadModel->download()->full_path()];
+ } else {
+ [progressView_ setMenu:activeDownloadMenu_];
+ }
+
+ [cell_ setStateFromDownload:downloadModel];
+}
+
+- (void)setIcon:(NSImage*)icon {
+ [cell_ setImage:icon];
+}
+
+- (void)remove {
+ // We are deleted after this!
+ [shelf_ remove:self];
+}
+
+- (void)updateVisibility:(id)sender {
+ if ([[self view] window])
+ [self updateTheme:[[[self view] window] themeProvider]];
+
+ // TODO(thakis): Make this prettier, by fading the items out or overlaying
+ // the partial visible one with a horizontal alpha gradient -- crbug.com/17830
+ NSView* view = [self view];
+ NSRect containerFrame = [[view superview] frame];
+ [view setHidden:(NSMaxX([view frame]) > NSWidth(containerFrame))];
+}
+
+- (IBAction)handleButtonClick:(id)sender {
+ NSEvent* event = [NSApp currentEvent];
+ if ([event modifierFlags] & NSCommandKeyMask) {
+ // Let cmd-click show the file in Finder, like e.g. in Safari and Spotlight.
+ menuBridge_->ExecuteCommand(DownloadShelfContextMenuMac::SHOW_IN_FOLDER);
+ } else {
+ DownloadItem* download = bridge_->download_model()->download();
+ download->OpenDownload();
+ }
+}
+
+- (NSSize)preferredSize {
+ if (state_ == kNormal)
+ return [progressView_ frame].size;
+ DCHECK_EQ(kDangerous, state_);
+ return [dangerousDownloadView_ frame].size;
+}
+
+- (DownloadItem*)download {
+ return bridge_->download_model()->download();
+}
+
+- (void)updateToolTip {
+ string16 elidedFilename = gfx::ElideFilename(
+ [self download]->GetFileNameToReportUser(),
+ gfx::Font(), kToolTipMaxWidth);
+ [progressView_ setToolTip:base::SysUTF16ToNSString(elidedFilename)];
+}
+
+- (void)clearDangerousMode {
+ [self setState:kNormal];
+ // The state change hide the dangerouse download view and is now showing the
+ // download progress view. This means the view is likely to be a different
+ // size, so trigger a shelf layout to fix up spacing.
+ [shelf_ layoutItems];
+}
+
+- (BOOL)isDangerousMode {
+ return state_ == kDangerous;
+}
+
+- (void)setState:(DownoadItemState)state {
+ if (state_ == state)
+ return;
+ state_ = state;
+ if (state_ == kNormal) {
+ [progressView_ setHidden:NO];
+ [dangerousDownloadView_ setHidden:YES];
+ } else {
+ DCHECK_EQ(kDangerous, state_);
+ [progressView_ setHidden:YES];
+ [dangerousDownloadView_ setHidden:NO];
+ }
+ // NOTE: Do not relayout the shelf, as this could get called during initial
+ // setup of the the item, so the localized text and sizing might not have
+ // happened yet.
+}
+
+// Called after the current theme has changed.
+- (void)themeDidChangeNotification:(NSNotification*)aNotification {
+ ThemeProvider* themeProvider =
+ static_cast<ThemeProvider*>([[aNotification object] pointerValue]);
+ [self updateTheme:themeProvider];
+}
+
+// Adapt appearance to the current theme. Called after theme changes and before
+// this is shown for the first time.
+- (void)updateTheme:(ThemeProvider*)themeProvider {
+ NSColor* color =
+ themeProvider->GetNSColor(BrowserThemeProvider::COLOR_TAB_TEXT, true);
+ [dangerousDownloadLabel_ setTextColor:color];
+}
+
+- (IBAction)saveDownload:(id)sender {
+ // The user has confirmed a dangerous download. We record how quickly the
+ // user did this to detect whether we're being clickjacked.
+ UMA_HISTOGRAM_LONG_TIMES("clickjacking.save_download",
+ base::Time::Now() - creationTime_);
+ // This will change the state and notify us.
+ bridge_->download_model()->download()->DangerousDownloadValidated();
+}
+
+- (IBAction)discardDownload:(id)sender {
+ UMA_HISTOGRAM_LONG_TIMES("clickjacking.discard_download",
+ base::Time::Now() - creationTime_);
+ if (bridge_->download_model()->download()->state() ==
+ DownloadItem::IN_PROGRESS)
+ bridge_->download_model()->download()->Cancel(true);
+ bridge_->download_model()->download()->Remove(true);
+ // WARNING: we are deleted at this point. Don't access 'this'.
+}
+
+
+// Sets the enabled and checked state of a particular menu item for this
+// download. We translate the NSMenuItem selection to menu selections understood
+// by the non platform specific download context menu.
+- (BOOL)validateMenuItem:(NSMenuItem *)item {
+ SEL action = [item action];
+
+ int actionId = 0;
+ if (action == @selector(handleOpen:)) {
+ actionId = DownloadShelfContextMenuMac::OPEN_WHEN_COMPLETE;
+ } else if (action == @selector(handleAlwaysOpen:)) {
+ actionId = DownloadShelfContextMenuMac::ALWAYS_OPEN_TYPE;
+ } else if (action == @selector(handleReveal:)) {
+ actionId = DownloadShelfContextMenuMac::SHOW_IN_FOLDER;
+ } else if (action == @selector(handleCancel:)) {
+ actionId = DownloadShelfContextMenuMac::CANCEL;
+ } else if (action == @selector(handleTogglePause:)) {
+ actionId = DownloadShelfContextMenuMac::TOGGLE_PAUSE;
+ } else {
+ NOTREACHED();
+ return YES;
+ }
+
+ if (menuBridge_->IsCommandIdChecked(actionId))
+ [item setState:NSOnState];
+ else
+ [item setState:NSOffState];
+
+ return menuBridge_->IsCommandIdEnabled(actionId) ? YES : NO;
+}
+
+- (IBAction)handleOpen:(id)sender {
+ menuBridge_->ExecuteCommand(
+ DownloadShelfContextMenuMac::OPEN_WHEN_COMPLETE);
+}
+
+- (IBAction)handleAlwaysOpen:(id)sender {
+ menuBridge_->ExecuteCommand(
+ DownloadShelfContextMenuMac::ALWAYS_OPEN_TYPE);
+}
+
+- (IBAction)handleReveal:(id)sender {
+ menuBridge_->ExecuteCommand(DownloadShelfContextMenuMac::SHOW_IN_FOLDER);
+}
+
+- (IBAction)handleCancel:(id)sender {
+ menuBridge_->ExecuteCommand(DownloadShelfContextMenuMac::CANCEL);
+}
+
+- (IBAction)handleTogglePause:(id)sender {
+ if([sender state] == NSOnState) {
+ [sender setTitle:l10n_util::GetNSStringWithFixup(
+ IDS_DOWNLOAD_MENU_PAUSE_ITEM)];
+ } else {
+ [sender setTitle:l10n_util::GetNSStringWithFixup(
+ IDS_DOWNLOAD_MENU_RESUME_ITEM)];
+ }
+ menuBridge_->ExecuteCommand(DownloadShelfContextMenuMac::TOGGLE_PAUSE);
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_item_mac.h b/chrome/browser/ui/cocoa/download/download_item_mac.h
new file mode 100644
index 0000000..4a4c7fe
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_mac.h
@@ -0,0 +1,63 @@
+// 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_UI_COCOA_DOWNLOAD_DOWNLOAD_ITEM_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_ITEM_MAC_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/cancelable_request.h"
+#include "chrome/browser/download/download_item.h"
+#include "chrome/browser/download/download_manager.h"
+#include "chrome/browser/icon_manager.h"
+
+class BaseDownloadItemModel;
+@class DownloadItemController;
+
+// A class that bridges the visible mac download items to chromium's download
+// model. The owning object (DownloadItemController) must explicitly call
+// |LoadIcon| if it wants to display the icon associated with this download.
+
+class DownloadItemMac : DownloadItem::Observer {
+ public:
+ // DownloadItemMac takes ownership of |download_model|.
+ DownloadItemMac(BaseDownloadItemModel* download_model,
+ DownloadItemController* controller);
+
+ // Destructor.
+ ~DownloadItemMac();
+
+ // DownloadItem::Observer implementation
+ virtual void OnDownloadUpdated(DownloadItem* download);
+ virtual void OnDownloadFileCompleted(DownloadItem* download) { }
+ virtual void OnDownloadOpened(DownloadItem* download) { }
+
+ BaseDownloadItemModel* download_model() { return download_model_.get(); }
+
+ // Asynchronous icon loading support.
+ void LoadIcon();
+
+ private:
+ // Callback for asynchronous icon loading.
+ void OnExtractIconComplete(IconManager::Handle handle, SkBitmap* icon_bitmap);
+
+ // The download item model we represent.
+ scoped_ptr<BaseDownloadItemModel> download_model_;
+
+ // The objective-c controller object.
+ DownloadItemController* item_controller_; // weak, owns us.
+
+ // For canceling an in progress icon request.
+ CancelableRequestConsumerT<int, 0> icon_consumer_;
+
+ // Stores the last known path where the file will be saved.
+ FilePath lastFilePath_;
+
+ DISALLOW_COPY_AND_ASSIGN(DownloadItemMac);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_ITEM_MAC_H_
diff --git a/chrome/browser/ui/cocoa/download/download_item_mac.mm b/chrome/browser/ui/cocoa/download/download_item_mac.mm
new file mode 100644
index 0000000..d6737ef
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_mac.mm
@@ -0,0 +1,96 @@
+// 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 "chrome/browser/ui/cocoa/download/download_item_mac.h"
+
+#include "base/callback.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/download/download_item.h"
+#include "chrome/browser/download/download_item_model.h"
+#import "chrome/browser/ui/cocoa/download/download_item_controller.h"
+#include "chrome/browser/ui/cocoa/download/download_util_mac.h"
+#include "skia/ext/skia_utils_mac.h"
+
+// DownloadItemMac -------------------------------------------------------------
+
+DownloadItemMac::DownloadItemMac(BaseDownloadItemModel* download_model,
+ DownloadItemController* controller)
+ : download_model_(download_model), item_controller_(controller) {
+ download_model_->download()->AddObserver(this);
+}
+
+DownloadItemMac::~DownloadItemMac() {
+ download_model_->download()->RemoveObserver(this);
+ icon_consumer_.CancelAllRequests();
+}
+
+void DownloadItemMac::OnDownloadUpdated(DownloadItem* download) {
+ DCHECK_EQ(download, download_model_->download());
+
+ if ([item_controller_ isDangerousMode] &&
+ download->safety_state() == DownloadItem::DANGEROUS_BUT_VALIDATED) {
+ // We have been approved.
+ [item_controller_ clearDangerousMode];
+ }
+
+ if (download->GetUserVerifiedFilePath() != lastFilePath_) {
+ // Turns out the file path is "unconfirmed %d.crdownload" for dangerous
+ // downloads. When the download is confirmed, the file is renamed on
+ // another thread, so reload the icon if the download filename changes.
+ LoadIcon();
+ lastFilePath_ = download->GetUserVerifiedFilePath();
+
+ [item_controller_ updateToolTip];
+ }
+
+ switch (download->state()) {
+ case DownloadItem::REMOVING:
+ [item_controller_ remove]; // We're deleted now!
+ break;
+ case DownloadItem::COMPLETE:
+ if (download->auto_opened()) {
+ [item_controller_ remove]; // We're deleted now!
+ return;
+ }
+ download_util::NotifySystemOfDownloadComplete(download->full_path());
+ // fall through
+ case DownloadItem::IN_PROGRESS:
+ case DownloadItem::CANCELLED:
+ [item_controller_ setStateFromDownload:download_model_.get()];
+ break;
+ default:
+ NOTREACHED();
+ }
+}
+
+void DownloadItemMac::LoadIcon() {
+ IconManager* icon_manager = g_browser_process->icon_manager();
+ if (!icon_manager) {
+ NOTREACHED();
+ return;
+ }
+
+ // We may already have this particular image cached.
+ FilePath file = download_model_->download()->GetUserVerifiedFilePath();
+ SkBitmap* icon_bitmap = icon_manager->LookupIcon(file, IconLoader::SMALL);
+ if (icon_bitmap) {
+ NSImage* icon = gfx::SkBitmapToNSImage(*icon_bitmap);
+ [item_controller_ setIcon:icon];
+ return;
+ }
+
+ // The icon isn't cached, load it asynchronously.
+ icon_manager->LoadIcon(file, IconLoader::SMALL, &icon_consumer_,
+ NewCallback(this,
+ &DownloadItemMac::OnExtractIconComplete));
+}
+
+void DownloadItemMac::OnExtractIconComplete(IconManager::Handle handle,
+ SkBitmap* icon_bitmap) {
+ if (!icon_bitmap)
+ return;
+
+ NSImage* icon = gfx::SkBitmapToNSImage(*icon_bitmap);
+ [item_controller_ setIcon:icon];
+}
diff --git a/chrome/browser/ui/cocoa/download/download_shelf_controller.h b/chrome/browser/ui/cocoa/download/download_shelf_controller.h
new file mode 100644
index 0000000..e67fab9
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_shelf_controller.h
@@ -0,0 +1,95 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#import "chrome/browser/ui/cocoa/view_resizer.h"
+
+@class AnimatableView;
+class BaseDownloadItemModel;
+class Browser;
+@class BrowserWindowController;
+@class DownloadItemController;
+class DownloadShelf;
+@class DownloadShelfView;
+@class HyperlinkButtonCell;
+
+// A controller class that manages the download shelf for one window. It is
+// responsible for the behavior of the shelf itself (showing/hiding, handling
+// the link, layout) as well as for managing the download items it contains.
+//
+// All the files in cocoa/downloads_* are related as follows:
+//
+// download_shelf_mac bridges calls from chromium's c++ world to the objc
+// download_shelf_controller for the shelf (this file). The shelf's background
+// is drawn by download_shelf_view. Every item in a shelf is controlled by a
+// download_item_controller.
+//
+// download_item_mac bridges calls from chromium's c++ world to the objc
+// download_item_controller, which is responsible for managing a single item
+// on the shelf. The item controller loads its UI from a xib file, where the
+// UI of an item itself is represented by a button that is drawn by
+// download_item_cell.
+
+@interface DownloadShelfController : NSViewController<NSTextViewDelegate> {
+ @private
+ IBOutlet HyperlinkButtonCell* showAllDownloadsCell_;
+
+ IBOutlet NSImageView* image_;
+
+ BOOL barIsVisible_;
+
+ scoped_ptr<DownloadShelf> bridge_;
+
+ // Height of the shelf when it's fully visible.
+ CGFloat maxShelfHeight_;
+
+ // Current height of the shelf. Changes while the shelf is animating in or
+ // out.
+ CGFloat currentShelfHeight_;
+
+ // The download items we have added to our shelf.
+ scoped_nsobject<NSMutableArray> downloadItemControllers_;
+
+ // The container that contains (and clamps) all the download items.
+ IBOutlet NSView* itemContainerView_;
+
+ // Delegate that handles resizing our view.
+ id<ViewResizer> resizeDelegate_;
+};
+
+- (id)initWithBrowser:(Browser*)browser
+ resizeDelegate:(id<ViewResizer>)resizeDelegate;
+
+- (IBAction)showDownloadsTab:(id)sender;
+
+// Returns our view cast as an AnimatableView.
+- (AnimatableView*)animatableView;
+
+- (DownloadShelf*)bridge;
+- (BOOL)isVisible;
+
+- (IBAction)show:(id)sender;
+
+// Run when the user clicks the close button on the right side of the shelf.
+- (IBAction)hide:(id)sender;
+
+- (void)addDownloadItem:(BaseDownloadItemModel*)model;
+
+// Remove a download, possibly via clearing browser data.
+- (void)remove:(DownloadItemController*)download;
+
+// Notification that we are closing and should release our downloads.
+- (void)exiting;
+
+// Return the height of the download shelf.
+- (float)height;
+
+// Re-layouts all download items based on their current state.
+- (void)layoutItems;
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_shelf_controller.mm b/chrome/browser/ui/cocoa/download/download_shelf_controller.mm
new file mode 100644
index 0000000..436fb13
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_shelf_controller.mm
@@ -0,0 +1,327 @@
+// 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/ui/cocoa/download/download_shelf_controller.h"
+
+#include "app/l10n_util.h"
+#include "app/resource_bundle.h"
+#include "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/download/download_item.h"
+#include "chrome/browser/download/download_manager.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/themes/browser_theme_provider.h"
+#include "chrome/browser/ui/browser.h"
+#import "chrome/browser/ui/cocoa/animatable_view.h"
+#include "chrome/browser/ui/cocoa/browser_window_cocoa.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#include "chrome/browser/ui/cocoa/download/download_item_controller.h"
+#include "chrome/browser/ui/cocoa/download/download_shelf_mac.h"
+#import "chrome/browser/ui/cocoa/download/download_shelf_view.h"
+#import "chrome/browser/ui/cocoa/hyperlink_button_cell.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+
+namespace {
+
+// Max number of download views we'll contain. Any time a view is added and
+// we already have this many download views, one is removed.
+const size_t kMaxDownloadItemCount = 16;
+
+// Horizontal padding between two download items.
+const int kDownloadItemPadding = 0;
+
+// Duration for the open-new-leftmost-item animation, in seconds.
+const NSTimeInterval kDownloadItemOpenDuration = 0.8;
+
+// Duration for download shelf closing animation, in seconds.
+const NSTimeInterval kDownloadShelfCloseDuration = 0.12;
+
+} // namespace
+
+@interface DownloadShelfController(Private)
+- (void)showDownloadShelf:(BOOL)enable;
+- (void)layoutItems:(BOOL)skipFirst;
+- (void)closed;
+
+- (void)updateTheme;
+- (void)themeDidChangeNotification:(NSNotification*)notification;
+- (void)viewFrameDidChange:(NSNotification*)notification;
+@end
+
+
+@implementation DownloadShelfController
+
+- (id)initWithBrowser:(Browser*)browser
+ resizeDelegate:(id<ViewResizer>)resizeDelegate {
+ if ((self = [super initWithNibName:@"DownloadShelf"
+ bundle:mac_util::MainAppBundle()])) {
+ resizeDelegate_ = resizeDelegate;
+ maxShelfHeight_ = NSHeight([[self view] bounds]);
+ currentShelfHeight_ = maxShelfHeight_;
+
+ // Reset the download shelf's frame height to zero. It will be properly
+ // positioned and sized the first time we try to set its height. (Just
+ // setting the rect to NSZeroRect does not work: it confuses Cocoa's view
+ // layout logic. If the shelf's width is too small, cocoa makes the download
+ // item container view wider than the browser window).
+ NSRect frame = [[self view] frame];
+ frame.size.height = 0;
+ [[self view] setFrame:frame];
+
+ downloadItemControllers_.reset([[NSMutableArray alloc] init]);
+
+ bridge_.reset(new DownloadShelfMac(browser, self));
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
+ [defaultCenter addObserver:self
+ selector:@selector(themeDidChangeNotification:)
+ name:kBrowserThemeDidChangeNotification
+ object:nil];
+
+ [[self animatableView] setResizeDelegate:resizeDelegate_];
+ [[self view] setPostsFrameChangedNotifications:YES];
+ [defaultCenter addObserver:self
+ selector:@selector(viewFrameDidChange:)
+ name:NSViewFrameDidChangeNotification
+ object:[self view]];
+
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ NSImage* favicon = rb.GetNativeImageNamed(IDR_DOWNLOADS_FAVICON);
+ DCHECK(favicon);
+ [image_ setImage:favicon];
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+
+ // The controllers will unregister themselves as observers when they are
+ // deallocated. No need to do that here.
+ [super dealloc];
+}
+
+// Called after the current theme has changed.
+- (void)themeDidChangeNotification:(NSNotification*)notification {
+ [self updateTheme];
+}
+
+// Called after the frame's rect has changed; usually when the height is
+// animated.
+- (void)viewFrameDidChange:(NSNotification*)notification {
+ // Anchor subviews at the top of |view|, so that it looks like the shelf
+ // is sliding out.
+ CGFloat newShelfHeight = NSHeight([[self view] frame]);
+ if (newShelfHeight == currentShelfHeight_)
+ return;
+
+ for (NSView* view in [[self view] subviews]) {
+ NSRect frame = [view frame];
+ frame.origin.y -= currentShelfHeight_ - newShelfHeight;
+ [view setFrame:frame];
+ }
+ currentShelfHeight_ = newShelfHeight;
+}
+
+// Adapt appearance to the current theme. Called after theme changes and before
+// this is shown for the first time.
+- (void)updateTheme {
+ NSColor* color = nil;
+
+ if (bridge_.get() && bridge_->browser() && bridge_->browser()->profile()) {
+ ThemeProvider* provider = bridge_->browser()->profile()->GetThemeProvider();
+
+ color =
+ provider->GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT, false);
+ }
+
+ if (!color)
+ color = [HyperlinkButtonCell defaultTextColor];
+
+ [showAllDownloadsCell_ setTextColor:color];
+}
+
+- (AnimatableView*)animatableView {
+ return static_cast<AnimatableView*>([self view]);
+}
+
+- (void)showDownloadsTab:(id)sender {
+ bridge_->browser()->ShowDownloadsTab();
+}
+
+- (void)remove:(DownloadItemController*)download {
+ // Look for the download in our controller array and remove it. This will
+ // explicity release it so that it removes itself as an Observer of the
+ // DownloadItem. We don't want to wait for autorelease since the DownloadItem
+ // we are observing will likely be gone by then.
+ [[NSNotificationCenter defaultCenter] removeObserver:download];
+
+ // TODO(dmaclach): Remove -- http://crbug.com/25845
+ [[download view] removeFromSuperview];
+
+ [downloadItemControllers_ removeObject:download];
+
+ [self layoutItems];
+
+ // Check to see if we have any downloads remaining and if not, hide the shelf.
+ if (![downloadItemControllers_ count])
+ [self showDownloadShelf:NO];
+}
+
+// We need to explicitly release our download controllers here since they need
+// to remove themselves as observers before the remaining shutdown happens.
+- (void)exiting {
+ [[self animatableView] stopAnimation];
+ downloadItemControllers_.reset();
+}
+
+// Show or hide the bar based on the value of |enable|. Handles animating the
+// resize of the content view.
+- (void)showDownloadShelf:(BOOL)enable {
+ if ([self isVisible] == enable)
+ return;
+
+ if ([[self view] window])
+ [self updateTheme];
+
+ // Animate the shelf out, but not in.
+ // TODO(rohitrao): We do not animate on the way in because Cocoa is already
+ // doing a lot of work to set up the download arrow animation. I've chosen to
+ // do no animation over janky animation. Find a way to make animating in
+ // smoother.
+ AnimatableView* view = [self animatableView];
+ if (enable)
+ [view setHeight:maxShelfHeight_];
+ else
+ [view animateToNewHeight:0 duration:kDownloadShelfCloseDuration];
+
+ barIsVisible_ = enable;
+}
+
+- (DownloadShelf*)bridge {
+ return bridge_.get();
+}
+
+- (BOOL)isVisible {
+ return barIsVisible_;
+}
+
+- (void)show:(id)sender {
+ [self showDownloadShelf:YES];
+}
+
+- (void)hide:(id)sender {
+ // If |sender| isn't nil, then we're being closed from the UI by the user and
+ // we need to tell our shelf implementation to close. Otherwise, we're being
+ // closed programmatically by our shelf implementation.
+ if (sender)
+ bridge_->Close();
+ else
+ [self showDownloadShelf:NO];
+}
+
+- (void)animationDidEnd:(NSAnimation*)animation {
+ if (![self isVisible])
+ [self closed];
+}
+
+- (float)height {
+ return maxShelfHeight_;
+}
+
+// If |skipFirst| is true, the frame of the leftmost item is not set.
+- (void)layoutItems:(BOOL)skipFirst {
+ CGFloat currentX = 0;
+ for (DownloadItemController* itemController
+ in downloadItemControllers_.get()) {
+ NSRect frame = [[itemController view] frame];
+ frame.origin.x = currentX;
+ frame.size.width = [itemController preferredSize].width;
+ if (!skipFirst)
+ [[[itemController view] animator] setFrame:frame];
+ currentX += frame.size.width + kDownloadItemPadding;
+ skipFirst = NO;
+ }
+}
+
+- (void)layoutItems {
+ [self layoutItems:NO];
+}
+
+- (void)addDownloadItem:(BaseDownloadItemModel*)model {
+ DCHECK([NSThread isMainThread]);
+ // Insert new item at the left.
+ scoped_nsobject<DownloadItemController> controller(
+ [[DownloadItemController alloc] initWithModel:model shelf:self]);
+
+ // Adding at index 0 in NSMutableArrays is O(1).
+ [downloadItemControllers_ insertObject:controller.get() atIndex:0];
+
+ [itemContainerView_ addSubview:[controller view]];
+
+ // The controller is in charge of removing itself as an observer in its
+ // dealloc.
+ [[NSNotificationCenter defaultCenter]
+ addObserver:controller
+ selector:@selector(updateVisibility:)
+ name:NSViewFrameDidChangeNotification
+ object:[controller view]];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:controller
+ selector:@selector(updateVisibility:)
+ name:NSViewFrameDidChangeNotification
+ object:itemContainerView_];
+
+ // Start at width 0...
+ NSSize size = [controller preferredSize];
+ NSRect frame = NSMakeRect(0, 0, 0, size.height);
+ [[controller view] setFrame:frame];
+
+ // ...then animate in
+ frame.size.width = size.width;
+ [NSAnimationContext beginGrouping];
+ [[NSAnimationContext currentContext]
+ gtm_setDuration:kDownloadItemOpenDuration
+ eventMask:NSLeftMouseUpMask];
+ [[[controller view] animator] setFrame:frame];
+ [NSAnimationContext endGrouping];
+
+ // Keep only a limited number of items in the shelf.
+ if ([downloadItemControllers_ count] > kMaxDownloadItemCount) {
+ DCHECK(kMaxDownloadItemCount > 0);
+
+ // Since no user will ever see the item being removed (needs a horizontal
+ // screen resolution greater than 3200 at 16 items at 200 pixels each),
+ // there's no point in animating the removal.
+ [self remove:[downloadItemControllers_ lastObject]];
+ }
+
+ // Finally, move the remaining items to the right. Skip the first item when
+ // laying out the items, so that the longer animation duration we set up above
+ // is not overwritten.
+ [self layoutItems:YES];
+}
+
+- (void)closed {
+ NSUInteger i = 0;
+ while (i < [downloadItemControllers_ count]) {
+ DownloadItemController* itemController =
+ [downloadItemControllers_ objectAtIndex:i];
+ bool isTransferDone =
+ [itemController download]->state() == DownloadItem::COMPLETE ||
+ [itemController download]->state() == DownloadItem::CANCELLED;
+ if (isTransferDone &&
+ [itemController download]->safety_state() != DownloadItem::DANGEROUS) {
+ [self remove:itemController];
+ } else {
+ ++i;
+ }
+ }
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_shelf_mac.h b/chrome/browser/ui/cocoa/download/download_shelf_mac.h
new file mode 100644
index 0000000..ddfc6f8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_shelf_mac.h
@@ -0,0 +1,43 @@
+// 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_UI_COCOA_DOWNLOAD_DOWNLOAD_SHELF_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_SHELF_MAC_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/download/download_shelf.h"
+
+class BaseDownloadItemModel;
+class CustomDrawButton;
+class DownloadItemMac;
+
+@class ShelfView;
+@class DownloadShelfController;
+
+// A class to bridge the chromium download shelf to mac gui. This is just a
+// wrapper class that forward everything to DownloadShelfController.
+
+class DownloadShelfMac : public DownloadShelf {
+ public:
+ explicit DownloadShelfMac(Browser* browser,
+ DownloadShelfController* controller);
+
+ // DownloadShelf implementation.
+ virtual void AddDownload(BaseDownloadItemModel* download_model);
+ virtual bool IsShowing() const;
+ virtual bool IsClosing() const;
+ virtual void Show();
+ virtual void Close();
+ virtual Browser* browser() const { return browser_; }
+
+ private:
+ // The browser that owns this shelf.
+ Browser* browser_;
+
+ DownloadShelfController* shelf_controller_; // weak, owns us
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_SHELF_MAC_H_
diff --git a/chrome/browser/ui/cocoa/download/download_shelf_mac.mm b/chrome/browser/ui/cocoa/download/download_shelf_mac.mm
new file mode 100644
index 0000000..53c13f4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_shelf_mac.mm
@@ -0,0 +1,40 @@
+// 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 "chrome/browser/ui/cocoa/download/download_shelf_mac.h"
+
+#include "chrome/browser/download/download_item_model.h"
+#include "chrome/browser/ui/browser.h"
+#import "chrome/browser/ui/cocoa/download/download_shelf_controller.h"
+#include "chrome/browser/ui/cocoa/download/download_item_mac.h"
+
+DownloadShelfMac::DownloadShelfMac(Browser* browser,
+ DownloadShelfController* controller)
+ : browser_(browser),
+ shelf_controller_(controller) {
+}
+
+void DownloadShelfMac::AddDownload(BaseDownloadItemModel* download_model) {
+ [shelf_controller_ addDownloadItem:download_model];
+ Show();
+}
+
+bool DownloadShelfMac::IsShowing() const {
+ return [shelf_controller_ isVisible] == YES;
+}
+
+bool DownloadShelfMac::IsClosing() const {
+ // TODO(estade): This is never called. For now just return false.
+ return false;
+}
+
+void DownloadShelfMac::Show() {
+ [shelf_controller_ show:nil];
+ browser_->UpdateDownloadShelfVisibility(true);
+}
+
+void DownloadShelfMac::Close() {
+ [shelf_controller_ hide:nil];
+ browser_->UpdateDownloadShelfVisibility(false);
+}
diff --git a/chrome/browser/ui/cocoa/download/download_shelf_mac_unittest.mm b/chrome/browser/ui/cocoa/download/download_shelf_mac_unittest.mm
new file mode 100644
index 0000000..961d2db
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_shelf_mac_unittest.mm
@@ -0,0 +1,91 @@
+// 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 "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/download/download_shelf_mac.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+// A fake implementation of DownloadShelfController. It implements only the
+// methods that DownloadShelfMac call during the tests in this file. We get this
+// class into the DownloadShelfMac constructor by some questionable casting --
+// Objective C is a dynamic language, so we pretend that's ok.
+
+@interface FakeDownloadShelfController : NSObject {
+ @public
+ int callCountIsVisible;
+ int callCountShow;
+ int callCountHide;
+}
+
+- (BOOL)isVisible;
+- (IBAction)show:(id)sender;
+- (IBAction)hide:(id)sender;
+@end
+
+@implementation FakeDownloadShelfController
+
+- (BOOL)isVisible {
+ ++callCountIsVisible;
+ return YES;
+}
+
+- (IBAction)show:(id)sender {
+ ++callCountShow;
+}
+
+- (IBAction)hide:(id)sender {
+ ++callCountHide;
+}
+
+@end
+
+
+namespace {
+
+class DownloadShelfMacTest : public CocoaTest {
+
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ shelf_controller_.reset([[FakeDownloadShelfController alloc] init]);
+ }
+
+ protected:
+ scoped_nsobject<FakeDownloadShelfController> shelf_controller_;
+ BrowserTestHelper browser_helper_;
+};
+
+TEST_F(DownloadShelfMacTest, CreationDoesNotCallShow) {
+ // Also make sure the DownloadShelfMacTest constructor doesn't crash.
+ DownloadShelfMac shelf(browser_helper_.browser(),
+ (DownloadShelfController*)shelf_controller_.get());
+ EXPECT_EQ(0, shelf_controller_.get()->callCountShow);
+}
+
+TEST_F(DownloadShelfMacTest, ForwardsShow) {
+ DownloadShelfMac shelf(browser_helper_.browser(),
+ (DownloadShelfController*)shelf_controller_.get());
+ EXPECT_EQ(0, shelf_controller_.get()->callCountShow);
+ shelf.Show();
+ EXPECT_EQ(1, shelf_controller_.get()->callCountShow);
+}
+
+TEST_F(DownloadShelfMacTest, ForwardsHide) {
+ DownloadShelfMac shelf(browser_helper_.browser(),
+ (DownloadShelfController*)shelf_controller_.get());
+ EXPECT_EQ(0, shelf_controller_.get()->callCountHide);
+ shelf.Close();
+ EXPECT_EQ(1, shelf_controller_.get()->callCountHide);
+}
+
+TEST_F(DownloadShelfMacTest, ForwardsIsShowing) {
+ DownloadShelfMac shelf(browser_helper_.browser(),
+ (DownloadShelfController*)shelf_controller_.get());
+ EXPECT_EQ(0, shelf_controller_.get()->callCountIsVisible);
+ shelf.IsShowing();
+ EXPECT_EQ(1, shelf_controller_.get()->callCountIsVisible);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/download/download_shelf_view.h b/chrome/browser/ui/cocoa/download/download_shelf_view.h
new file mode 100644
index 0000000..bcd949c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_shelf_view.h
@@ -0,0 +1,20 @@
+// 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_UI_COCOA_DOWNLOAD_DOWNLOAD_SHELF_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_SHELF_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/animatable_view.h"
+
+// A view that handles any special rendering for the download shelf, painting
+// a gradient and managing a set of DownloadItemViews.
+
+@interface DownloadShelfView : AnimatableView {
+}
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_SHELF_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/download/download_shelf_view.mm b/chrome/browser/ui/cocoa/download/download_shelf_view.mm
new file mode 100644
index 0000000..f3840ef
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_shelf_view.mm
@@ -0,0 +1,71 @@
+// 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/ui/cocoa/download/download_shelf_view.h"
+
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+#include "grit/theme_resources.h"
+
+@implementation DownloadShelfView
+
+- (NSColor*)strokeColor {
+ BOOL isKey = [[self window] isKeyWindow];
+ ThemeProvider* themeProvider = [[self window] themeProvider];
+ return themeProvider ? themeProvider->GetNSColor(
+ isKey ? BrowserThemeProvider::COLOR_TOOLBAR_STROKE :
+ BrowserThemeProvider::COLOR_TOOLBAR_STROKE_INACTIVE, true) :
+ [NSColor blackColor];
+}
+
+- (void)drawRect:(NSRect)rect {
+ BOOL isKey = [[self window] isKeyWindow];
+ ThemeProvider* themeProvider = [[self window] themeProvider];
+ if (!themeProvider)
+ return;
+
+ NSColor* backgroundImageColor =
+ themeProvider->GetNSImageColorNamed(IDR_THEME_TOOLBAR, false);
+ if (backgroundImageColor) {
+ // We want our backgrounds for the shelf to be phased from the upper
+ // left hand corner of the view.
+ NSPoint phase = NSMakePoint(0, NSHeight([self bounds]));
+ [[NSGraphicsContext currentContext] setPatternPhase:phase];
+ [backgroundImageColor set];
+ NSRectFill([self bounds]);
+ } else {
+ NSGradient* gradient = themeProvider->GetNSGradient(
+ isKey ? BrowserThemeProvider::GRADIENT_TOOLBAR :
+ BrowserThemeProvider::GRADIENT_TOOLBAR_INACTIVE);
+ NSPoint startPoint = [self convertPoint:NSMakePoint(0, 0) fromView:nil];
+ NSPoint endPoint =
+ [self convertPoint:NSMakePoint(0, [self frame].size.height)
+ fromView:nil];
+
+ [gradient drawFromPoint:startPoint
+ toPoint:endPoint
+ options:NSGradientDrawsBeforeStartingLocation |
+ NSGradientDrawsAfterEndingLocation];
+ }
+
+ // Draw top stroke
+ [[self strokeColor] set];
+ NSRect borderRect, contentRect;
+ NSDivideRect([self bounds], &borderRect, &contentRect, 1, NSMaxYEdge);
+ NSRectFillUsingOperation(borderRect, NSCompositeSourceOver);
+}
+
+// Mouse down events on the download shelf should not allow dragging the parent
+// window around.
+- (BOOL)mouseDownCanMoveWindow {
+ return NO;
+}
+
+- (ViewID)viewID {
+ return VIEW_ID_DOWNLOAD_SHELF;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_shelf_view_unittest.mm b/chrome/browser/ui/cocoa/download/download_shelf_view_unittest.mm
new file mode 100644
index 0000000..926593f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_shelf_view_unittest.mm
@@ -0,0 +1,23 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/download/download_shelf_view.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class DownloadShelfViewTest : public CocoaTest {
+};
+
+// This class only needs to do one thing: prevent mouse down events from moving
+// the parent window around.
+TEST_F(DownloadShelfViewTest, CanDragWindow) {
+ scoped_nsobject<DownloadShelfView> view([[DownloadShelfView alloc] init]);
+ EXPECT_FALSE([view mouseDownCanMoveWindow]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/download/download_started_animation_mac.mm b/chrome/browser/ui/cocoa/download/download_started_animation_mac.mm
new file mode 100644
index 0000000..3bf29d5
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_started_animation_mac.mm
@@ -0,0 +1,195 @@
+// 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.
+//
+// This file contains the Mac implementation the download animation, displayed
+// at the start of a download. The animation produces an arrow pointing
+// downwards and animates towards the bottom of the window where the new
+// download appears in the download shelf.
+
+#include "chrome/browser/download/download_started_animation.h"
+
+#import <QuartzCore/QuartzCore.h>
+
+#include "app/resource_bundle.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents/tab_contents_view_mac.h"
+#include "chrome/common/notification_registrar.h"
+#include "chrome/common/notification_service.h"
+#import "chrome/browser/ui/cocoa/animatable_image.h"
+#include "grit/theme_resources.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+#include "third_party/skia/include/utils/mac/SkCGUtils.h"
+
+class DownloadAnimationTabObserver;
+
+// A class for managing the Core Animation download animation.
+// Should be instantiated using +startAnimationWithTabContents:.
+@interface DownloadStartedAnimationMac : NSObject {
+ @private
+ // The observer for the TabContents we are drawing on.
+ scoped_ptr<DownloadAnimationTabObserver> observer_;
+ CGFloat imageWidth_;
+ AnimatableImage* animation_;
+};
+
++ (void)startAnimationWithTabContents:(TabContents*)tabContents;
+
+// Called by the Observer if the tab is hidden or closed.
+- (void)closeAnimation;
+
+@end
+
+// A helper class to monitor tab hidden and closed notifications. If we receive
+// such a notification, we stop the animation.
+class DownloadAnimationTabObserver : public NotificationObserver {
+ public:
+ DownloadAnimationTabObserver(DownloadStartedAnimationMac* owner,
+ TabContents* tab_contents)
+ : owner_(owner),
+ tab_contents_(tab_contents) {
+ registrar_.Add(this,
+ NotificationType::TAB_CONTENTS_HIDDEN,
+ Source<TabContents>(tab_contents_));
+ registrar_.Add(this,
+ NotificationType::TAB_CONTENTS_DESTROYED,
+ Source<TabContents>(tab_contents_));
+ }
+
+ // Runs when a tab is hidden or destroyed. Let our owner know we should end
+ // the animation.
+ void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ // This ends up deleting us.
+ [owner_ closeAnimation];
+ }
+
+ private:
+ // The object we need to inform when we get a notification. Weak.
+ DownloadStartedAnimationMac* owner_;
+
+ // The tab we are observing. Weak.
+ TabContents* tab_contents_;
+
+ // Used for registering to receive notifications and automatic clean up.
+ NotificationRegistrar registrar_;
+
+ DISALLOW_COPY_AND_ASSIGN(DownloadAnimationTabObserver);
+};
+
+@implementation DownloadStartedAnimationMac
+
+- (id)initWithTabContents:(TabContents*)tabContents {
+ if ((self = [super init])) {
+ // Load the image of the download arrow.
+ ResourceBundle& bundle = ResourceBundle::GetSharedInstance();
+ NSImage* image = bundle.GetNativeImageNamed(IDR_DOWNLOAD_ANIMATION_BEGIN);
+
+ // Figure out the positioning in the current tab. Try to position the layer
+ // against the left edge, and three times the download image's height from
+ // the bottom of the tab, assuming there is enough room. If there isn't
+ // enough, don't show the animation and let the shelf speak for itself.
+ gfx::Rect bounds;
+ tabContents->GetContainerBounds(&bounds);
+ imageWidth_ = [image size].width;
+ CGFloat imageHeight = [image size].height;
+
+ // Sanity check the size in case there's no room to display the animation.
+ if (bounds.height() < imageHeight) {
+ [self release];
+ return nil;
+ }
+
+ NSView* tabContentsView = tabContents->GetNativeView();
+ NSWindow* parentWindow = [tabContentsView window];
+ if (!parentWindow) {
+ // The tab is no longer frontmost.
+ [self release];
+ return nil;
+ }
+
+ NSPoint origin = [tabContentsView frame].origin;
+ origin = [tabContentsView convertPoint:origin toView:nil];
+ origin = [parentWindow convertBaseToScreen:origin];
+
+ // Create the animation object to assist in animating and fading.
+ CGFloat animationHeight = MIN(bounds.height(), 4 * imageHeight);
+ NSRect frame = NSMakeRect(origin.x, origin.y, imageWidth_, animationHeight);
+ animation_ = [[AnimatableImage alloc] initWithImage:image
+ animationFrame:frame];
+ [parentWindow addChildWindow:animation_ ordered:NSWindowAbove];
+
+ animationHeight = MIN(bounds.height(), 3 * imageHeight);
+ [animation_ setStartFrame:CGRectMake(0, animationHeight,
+ imageWidth_, imageHeight)];
+ [animation_ setEndFrame:CGRectMake(0, imageHeight,
+ imageWidth_, imageHeight)];
+ [animation_ setStartOpacity:1.0];
+ [animation_ setEndOpacity:0.4];
+ [animation_ setDuration:0.6];
+
+ observer_.reset(new DownloadAnimationTabObserver(self, tabContents));
+
+ // Set up to get notified about resize events on the parent window.
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
+ selector:@selector(parentWindowChanged:)
+ name:NSWindowDidResizeNotification
+ object:parentWindow];
+ // When the animation window closes, it needs to be removed from the
+ // parent window.
+ [center addObserver:self
+ selector:@selector(windowWillClose:)
+ name:NSWindowWillCloseNotification
+ object:animation_];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+// Called when the parent window is resized.
+- (void)parentWindowChanged:(NSNotification*)notification {
+ NSWindow* parentWindow = [animation_ parentWindow];
+ DCHECK([[notification object] isEqual:parentWindow]);
+ NSRect parentFrame = [parentWindow frame];
+ NSRect frame = parentFrame;
+ frame.size.width = MIN(imageWidth_, NSWidth(parentFrame));
+ [animation_ setFrame:frame display:YES];
+}
+
+- (void)closeAnimation {
+ [animation_ close];
+}
+
+// When the animation closes, release self.
+- (void)windowWillClose:(NSNotification*)notification {
+ DCHECK([[notification object] isEqual:animation_]);
+ [[animation_ parentWindow] removeChildWindow:animation_];
+ [self release];
+}
+
++ (void)startAnimationWithTabContents:(TabContents*)contents {
+ // Will be deleted when the animation window closes.
+ DownloadStartedAnimationMac* controller =
+ [[self alloc] initWithTabContents:contents];
+ // The initializer can return nil.
+ if (!controller)
+ return;
+
+ // The |animation_| releases itself when done.
+ [controller->animation_ startAnimation];
+}
+
+@end
+
+void DownloadStartedAnimation::Show(TabContents* tab_contents) {
+ DCHECK(tab_contents);
+
+ // Will be deleted when the animation is complete.
+ [DownloadStartedAnimationMac startAnimationWithTabContents:tab_contents];
+}
diff --git a/chrome/browser/ui/cocoa/download/download_util_mac.h b/chrome/browser/ui/cocoa/download/download_util_mac.h
new file mode 100644
index 0000000..8f99c8b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_util_mac.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.
+//
+// Download utility functions for Mac OS X.
+
+#ifndef CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_UTIL_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_UTIL_MAC_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+class FilePath;
+
+namespace download_util {
+
+void AddFileToPasteboard(NSPasteboard* pasteboard, const FilePath& path);
+
+// Notify the system that a download completed. This will cause the download
+// folder in the dock to bounce.
+void NotifySystemOfDownloadComplete(const FilePath& path);
+
+} // namespace download_util
+
+#endif // CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_UTIL_MAC_H_
diff --git a/chrome/browser/ui/cocoa/download/download_util_mac.mm b/chrome/browser/ui/cocoa/download/download_util_mac.mm
new file mode 100644
index 0000000..baafbbf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_util_mac.mm
@@ -0,0 +1,83 @@
+// 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.
+//
+// Download utility implementation for Mac OS X.
+
+#include "chrome/browser/ui/cocoa/download/download_util_mac.h"
+
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/download/download_item.h"
+#include "chrome/browser/download/download_manager.h"
+#import "chrome/browser/ui/cocoa/dock_icon.h"
+#include "gfx/native_widget_types.h"
+#include "skia/ext/skia_utils_mac.h"
+
+namespace download_util {
+
+void AddFileToPasteboard(NSPasteboard* pasteboard, const FilePath& path) {
+ // Write information about the file being dragged to the pasteboard.
+ NSString* file = base::SysUTF8ToNSString(path.value());
+ NSArray* fileList = [NSArray arrayWithObject:file];
+ [pasteboard declareTypes:[NSArray arrayWithObject:NSFilenamesPboardType]
+ owner:nil];
+ [pasteboard setPropertyList:fileList forType:NSFilenamesPboardType];
+}
+
+void NotifySystemOfDownloadComplete(const FilePath& path) {
+ NSString* filePath = base::SysUTF8ToNSString(path.value());
+ [[NSDistributedNotificationCenter defaultCenter]
+ postNotificationName:@"com.apple.DownloadFileFinished"
+ object:filePath];
+
+ NSString* parentPath = [filePath stringByDeletingLastPathComponent];
+ FNNotifyByPath(
+ reinterpret_cast<const UInt8*>([parentPath fileSystemRepresentation]),
+ kFNDirectoryModifiedMessage,
+ kNilOptions);
+}
+
+void DragDownload(const DownloadItem* download,
+ SkBitmap* icon,
+ gfx::NativeView view) {
+ NSPasteboard* pasteboard = [NSPasteboard pasteboardWithName:NSDragPboard];
+ AddFileToPasteboard(pasteboard, download->full_path());
+
+ // Convert to an NSImage.
+ NSImage* dragImage = gfx::SkBitmapToNSImage(*icon);
+
+ // Synthesize a drag event, since we don't have access to the actual event
+ // that initiated a drag (possibly consumed by the DOM UI, for example).
+ NSPoint position = [[view window] mouseLocationOutsideOfEventStream];
+ NSTimeInterval eventTime = [[NSApp currentEvent] timestamp];
+ NSEvent* dragEvent = [NSEvent mouseEventWithType:NSLeftMouseDragged
+ location:position
+ modifierFlags:NSLeftMouseDraggedMask
+ timestamp:eventTime
+ windowNumber:[[view window] windowNumber]
+ context:nil
+ eventNumber:0
+ clickCount:1
+ pressure:1.0];
+
+ // Run the drag operation.
+ [[view window] dragImage:dragImage
+ at:position
+ offset:NSZeroSize
+ event:dragEvent
+ pasteboard:pasteboard
+ source:view
+ slideBack:YES];
+}
+
+void UpdateAppIconDownloadProgress(int download_count,
+ bool progress_known,
+ float progress) {
+ DockIcon* dock_icon = [DockIcon sharedDockIcon];
+ [dock_icon setDownloads:download_count];
+ [dock_icon setIndeterminate:!progress_known];
+ [dock_icon setProgress:progress];
+ [dock_icon updateIcon];
+}
+
+} // namespace download_util
diff --git a/chrome/browser/ui/cocoa/download/download_util_mac_unittest.mm b/chrome/browser/ui/cocoa/download/download_util_mac_unittest.mm
new file mode 100644
index 0000000..bd99e02
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_util_mac_unittest.mm
@@ -0,0 +1,58 @@
+// 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.
+
+// Download utility test for Mac OS X.
+
+#include "base/path_service.h"
+#include "base/sys_string_conversions.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/download/download_util_mac.h"
+#include "chrome/common/chrome_paths.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class DownloadUtilMacTest : public CocoaTest {
+ public:
+ DownloadUtilMacTest() {
+ pasteboard_ = [NSPasteboard pasteboardWithUniqueName];
+ }
+
+ virtual ~DownloadUtilMacTest() {
+ [pasteboard_ releaseGlobally];
+ }
+
+ NSPasteboard* const pasteboard() { return pasteboard_; }
+
+ private:
+ NSPasteboard* pasteboard_;
+};
+
+// Ensure adding files to the pasteboard methods works as expected.
+TEST_F(DownloadUtilMacTest, AddFileToPasteboardTest) {
+ // Get a download test file for addition to the pasteboard.
+ FilePath testPath;
+ ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &testPath));
+ FilePath testFile(FILE_PATH_LITERAL("download-test1.lib"));
+ testPath = testPath.Append(testFile);
+
+ // Add a test file to the pasteboard via the download_util method.
+ download_util::AddFileToPasteboard(pasteboard(), testPath);
+
+ // Test to see that the object type for dragging files is available.
+ NSArray* types = [NSArray arrayWithObject:NSFilenamesPboardType];
+ NSString* available = [pasteboard() availableTypeFromArray:types];
+ EXPECT_TRUE(available != nil);
+
+ // Ensure the path is what we expect.
+ NSArray* files = [pasteboard() propertyListForType:NSFilenamesPboardType];
+ ASSERT_TRUE(files != nil);
+ NSString* expectedPath = [files objectAtIndex:0];
+ NSString* realPath = base::SysWideToNSString(testPath.ToWStringHack());
+ EXPECT_NSEQ(expectedPath, realPath);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/draggable_button.h b/chrome/browser/ui/cocoa/draggable_button.h
new file mode 100644
index 0000000..2e166b7
--- /dev/null
+++ b/chrome/browser/ui/cocoa/draggable_button.h
@@ -0,0 +1,33 @@
+// 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>
+
+// Class for buttons that can be drag sources. If the mouse is clicked and moved
+// more than a given distance, this class will call |-beginDrag:| instead of
+// |-performClick:|. Subclasses should override these two methods.
+@interface DraggableButton : NSButton {
+ @private
+ BOOL draggable_; // Is this a draggable type of button?
+}
+
+// Enable or disable dragability for special buttons like "Other Bookmarks".
+@property (nonatomic) BOOL draggable;
+
+// Called when a drag should start. Subclasses must override this to do any
+// pasteboard manipulation and begin the drag, usually with
+// -dragImage:at:offset:event:. Subclasses must call one of the blocking
+// -drag* methods of NSView when overriding this method.
+- (void)beginDrag:(NSEvent*)dragEvent;
+
+@end // @interface DraggableButton
+
+@interface DraggableButton (Private)
+
+// Resets the draggable state of the button after dragging is finished. This is
+// called by DraggableButton when the beginDrag call returns, it should not be
+// called by the subclass.
+- (void)endDrag;
+
+@end // @interface DraggableButton(Private)
diff --git a/chrome/browser/ui/cocoa/draggable_button.mm b/chrome/browser/ui/cocoa/draggable_button.mm
new file mode 100644
index 0000000..923476b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/draggable_button.mm
@@ -0,0 +1,150 @@
+// 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/ui/cocoa/draggable_button.h"
+
+#include "base/logging.h"
+#import "base/scoped_nsobject.h"
+
+namespace {
+
+// Code taken from <http://codereview.chromium.org/180036/diff/3001/3004>.
+// TODO(viettrungluu): Do we want common, standard code for drag hysteresis?
+const CGFloat kWebDragStartHysteresisX = 5.0;
+const CGFloat kWebDragStartHysteresisY = 5.0;
+const CGFloat kDragExpirationTimeout = 1.0;
+
+}
+
+@implementation DraggableButton
+
+@synthesize draggable = draggable_;
+
+- (id)initWithFrame:(NSRect)frame {
+ if ((self = [super initWithFrame:frame])) {
+ draggable_ = YES;
+ }
+ return self;
+}
+
+- (id)initWithCoder:(NSCoder*)coder {
+ if ((self = [super initWithCoder:coder])) {
+ draggable_ = YES;
+ }
+ return self;
+}
+
+// Determine whether a mouse down should turn into a drag; started as copy of
+// NSTableView code.
+- (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent
+ withExpiration:(NSDate*)expiration
+ xHysteresis:(float)xHysteresis
+ yHysteresis:(float)yHysteresis {
+ if ([mouseDownEvent type] != NSLeftMouseDown) {
+ return NO;
+ }
+
+ NSEvent* nextEvent = nil;
+ NSEvent* firstEvent = nil;
+ NSEvent* dragEvent = nil;
+ NSEvent* mouseUp = nil;
+ BOOL dragIt = NO;
+
+ while ((nextEvent = [[self window]
+ nextEventMatchingMask:(NSLeftMouseUpMask | NSLeftMouseDraggedMask)
+ untilDate:expiration
+ inMode:NSEventTrackingRunLoopMode
+ dequeue:YES]) != nil) {
+ if (firstEvent == nil) {
+ firstEvent = nextEvent;
+ }
+ if ([nextEvent type] == NSLeftMouseDragged) {
+ float deltax = ABS([nextEvent locationInWindow].x -
+ [mouseDownEvent locationInWindow].x);
+ float deltay = ABS([nextEvent locationInWindow].y -
+ [mouseDownEvent locationInWindow].y);
+ dragEvent = nextEvent;
+ if (deltax >= xHysteresis) {
+ dragIt = YES;
+ break;
+ }
+ if (deltay >= yHysteresis) {
+ dragIt = YES;
+ break;
+ }
+ } else if ([nextEvent type] == NSLeftMouseUp) {
+ mouseUp = nextEvent;
+ break;
+ }
+ }
+
+ // Since we've been dequeuing the events (If we don't, we'll never see
+ // the mouse up...), we need to push some of the events back on.
+ // It makes sense to put the first and last drag events and the mouse
+ // up if there was one.
+ if (mouseUp != nil) {
+ [NSApp postEvent:mouseUp atStart:YES];
+ }
+ if (dragEvent != nil) {
+ [NSApp postEvent:dragEvent atStart:YES];
+ }
+ if (firstEvent != mouseUp && firstEvent != dragEvent) {
+ [NSApp postEvent:firstEvent atStart:YES];
+ }
+
+ return dragIt;
+}
+
+- (BOOL)dragShouldBeginFromMouseDown:(NSEvent*)mouseDownEvent
+ withExpiration:(NSDate*)expiration {
+ return [self dragShouldBeginFromMouseDown:mouseDownEvent
+ withExpiration:expiration
+ xHysteresis:kWebDragStartHysteresisX
+ yHysteresis:kWebDragStartHysteresisY];
+}
+
+- (void)mouseUp:(NSEvent*)theEvent {
+ if (!draggable_) {
+ [super mouseUp:theEvent];
+ return;
+ }
+
+ // There are non-drag cases where a mouseUp: may happen
+ // (e.g. mouse-down, cmd-tab to another application, move mouse,
+ // mouse-up). So we check.
+ NSPoint viewLocal = [self convertPoint:[theEvent locationInWindow]
+ fromView:[[self window] contentView]];
+ if (NSPointInRect(viewLocal, [self bounds])) {
+ [self performClick:self];
+ }
+}
+
+// Mimic "begin a click" operation visually. Do NOT follow through
+// with normal button event handling.
+- (void)mouseDown:(NSEvent*)theEvent {
+ if (draggable_) {
+ [[self cell] setHighlighted:YES];
+ NSDate* date = [NSDate dateWithTimeIntervalSinceNow:kDragExpirationTimeout];
+ if ([self dragShouldBeginFromMouseDown:theEvent
+ withExpiration:date]) {
+ [self beginDrag:theEvent];
+ [self endDrag];
+ } else {
+ [super mouseDown:theEvent];
+ }
+ } else {
+ [super mouseDown:theEvent];
+ }
+}
+
+- (void)beginDrag:(NSEvent*)dragEvent {
+ // Must be overridden by subclasses.
+ NOTREACHED();
+}
+
+- (void)endDrag {
+ [[self cell] setHighlighted:NO];
+}
+
+@end // @interface DraggableButton
diff --git a/chrome/browser/ui/cocoa/draggable_button_unittest.mm b/chrome/browser/ui/cocoa/draggable_button_unittest.mm
new file mode 100644
index 0000000..2700a49
--- /dev/null
+++ b/chrome/browser/ui/cocoa/draggable_button_unittest.mm
@@ -0,0 +1,137 @@
+// 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/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/draggable_button.h"
+#import "chrome/browser/ui/cocoa/test_event_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface TestableDraggableButton : DraggableButton {
+ NSUInteger dragCount_;
+ BOOL wasTriggered_;
+}
+- (void)trigger:(id)sender;
+- (BOOL)wasTriggered;
+- (NSUInteger)dragCount;
+@end
+
+@implementation TestableDraggableButton
+- (id)initWithFrame:(NSRect)frame {
+ if ((self = [super initWithFrame:frame])) {
+ dragCount_ = 0;
+ wasTriggered_ = NO;
+ }
+ return self;
+}
+- (void)beginDrag:(NSEvent*)theEvent {
+ dragCount_++;
+}
+
+- (void)trigger:(id)sender {
+ wasTriggered_ = YES;
+}
+
+- (BOOL)wasTriggered {
+ return wasTriggered_;
+}
+
+- (NSUInteger)dragCount {
+ return dragCount_;
+}
+@end
+
+class DraggableButtonTest : public CocoaTest {};
+
+// Make sure the basic case of "click" still works.
+TEST_F(DraggableButtonTest, DownUp) {
+ scoped_nsobject<TestableDraggableButton> button(
+ [[TestableDraggableButton alloc] initWithFrame:NSMakeRect(0,0,500,500)]);
+ [[test_window() contentView] addSubview:button.get()];
+ [button setTarget:button];
+ [button setAction:@selector(trigger:)];
+ EXPECT_FALSE([button wasTriggered]);
+ NSEvent* downEvent =
+ test_event_utils::MouseEventAtPoint(NSMakePoint(10,10),
+ NSLeftMouseDown, 0);
+ NSEvent* upEvent =
+ test_event_utils::MouseEventAtPoint(NSMakePoint(10,10),
+ NSLeftMouseUp, 0);
+ [NSApp postEvent:upEvent atStart:YES];
+ [test_window() sendEvent:downEvent];
+ EXPECT_TRUE([button wasTriggered]); // confirms target/action fired
+}
+
+TEST_F(DraggableButtonTest, DraggableHysteresis) {
+ scoped_nsobject<TestableDraggableButton> button(
+ [[TestableDraggableButton alloc] initWithFrame:NSMakeRect(0,0,500,500)]);
+ [[test_window() contentView] addSubview:button.get()];
+ NSEvent* downEvent =
+ test_event_utils::MouseEventAtPoint(NSMakePoint(10,10),
+ NSLeftMouseDown,
+ 0);
+ NSEvent* firstMove =
+ test_event_utils::MouseEventAtPoint(NSMakePoint(11,11),
+ NSLeftMouseDragged,
+ 0);
+ NSEvent* firstUpEvent =
+ test_event_utils::MouseEventAtPoint(NSMakePoint(11,11),
+ NSLeftMouseUp,
+ 0);
+ NSEvent* secondMove =
+ test_event_utils::MouseEventAtPoint(NSMakePoint(100,100),
+ NSLeftMouseDragged,
+ 0);
+ NSEvent* secondUpEvent =
+ test_event_utils::MouseEventAtPoint(NSMakePoint(100,100),
+ NSLeftMouseUp,
+ 0);
+ // If the mouse only moves one pixel in each direction
+ // it should not cause a drag.
+ [NSApp postEvent:firstUpEvent atStart:YES];
+ [NSApp postEvent:firstMove atStart:YES];
+ [button mouseDown:downEvent];
+ EXPECT_EQ(0U, [button dragCount]);
+
+ // If the mouse moves > 5 pixels in either direciton
+ // it should cause a drag.
+ [NSApp postEvent:secondUpEvent atStart:YES];
+ [NSApp postEvent:secondMove atStart:YES];
+ [button mouseDown:downEvent];
+ EXPECT_EQ(1U, [button dragCount]);
+}
+
+TEST_F(DraggableButtonTest, ResetState) {
+ scoped_nsobject<TestableDraggableButton> button(
+ [[TestableDraggableButton alloc] initWithFrame:NSMakeRect(0,0,500,500)]);
+ [[test_window() contentView] addSubview:button.get()];
+ NSEvent* downEvent =
+ test_event_utils::MouseEventAtPoint(NSMakePoint(10,10),
+ NSLeftMouseDown,
+ 0);
+ NSEvent* moveEvent =
+ test_event_utils::MouseEventAtPoint(NSMakePoint(100,100),
+ NSLeftMouseDragged,
+ 0);
+ NSEvent* upEvent =
+ test_event_utils::MouseEventAtPoint(NSMakePoint(100,100),
+ NSLeftMouseUp,
+ 0);
+ // If the mouse moves > 5 pixels in either direciton it should cause a drag.
+ [NSApp postEvent:upEvent atStart:YES];
+ [NSApp postEvent:moveEvent atStart:YES];
+ [button mouseDown:downEvent];
+
+ // The button should not be highlighted after the drag finishes.
+ EXPECT_FALSE([[button cell] isHighlighted]);
+ EXPECT_EQ(1U, [button dragCount]);
+
+ // We should be able to initiate another drag immediately after the first one.
+ [NSApp postEvent:upEvent atStart:YES];
+ [NSApp postEvent:moveEvent atStart:YES];
+ [button mouseDown:downEvent];
+ EXPECT_EQ(2U, [button dragCount]);
+ EXPECT_FALSE([[button cell] isHighlighted]);
+}
diff --git a/chrome/browser/ui/cocoa/edit_search_engine_cocoa_controller.h b/chrome/browser/ui/cocoa/edit_search_engine_cocoa_controller.h
new file mode 100644
index 0000000..b1bfabc
--- /dev/null
+++ b/chrome/browser/ui/cocoa/edit_search_engine_cocoa_controller.h
@@ -0,0 +1,53 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+class TemplateURL;
+
+#include "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/search_engines/edit_search_engine_controller.h"
+
+// This controller presents a dialog that allows a user to add or edit a search
+// engine. If constructed with a nil |templateURL| then it is an add operation,
+// otherwise it will modify the passed URL. A |delegate| is necessary to
+// perform the actual database modifications, and should probably be an
+// instance of KeywordEditorModelObserver.
+
+@interface EditSearchEngineCocoaController :
+ NSWindowController<NSWindowDelegate> {
+ IBOutlet NSTextField* nameField_;
+ IBOutlet NSTextField* keywordField_;
+ IBOutlet NSTextField* urlField_;
+ IBOutlet NSImageView* nameImage_;
+ IBOutlet NSImageView* keywordImage_;
+ IBOutlet NSImageView* urlImage_;
+ IBOutlet NSButton* doneButton_;
+ IBOutlet NSTextField* urlDescriptionField_;
+ IBOutlet NSView* labelContainer_;
+ IBOutlet NSBox* fieldAndImageContainer_;
+
+ // Refs to the good and bad images used in the interface validation.
+ scoped_nsobject<NSImage> goodImage_;
+ scoped_nsobject<NSImage> badImage_;
+
+ Profile* profile_; // weak
+ const TemplateURL* templateURL_; // weak
+ scoped_ptr<EditSearchEngineController> controller_;
+}
+
+- (id)initWithProfile:(Profile*)profile
+ delegate:(EditSearchEngineControllerDelegate*)delegate
+ templateURL:(const TemplateURL*)url;
+
+- (IBAction)cancel:(id)sender;
+- (IBAction)save:(id)sender;
+
+@end
+
+@interface EditSearchEngineCocoaController (ExposedForTesting)
+- (BOOL)validateFields;
+@end
diff --git a/chrome/browser/ui/cocoa/edit_search_engine_cocoa_controller.mm b/chrome/browser/ui/cocoa/edit_search_engine_cocoa_controller.mm
new file mode 100644
index 0000000..06fc94b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/edit_search_engine_cocoa_controller.mm
@@ -0,0 +1,187 @@
+// 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/ui/cocoa/edit_search_engine_cocoa_controller.h"
+
+#include "app/l10n_util_mac.h"
+#include "app/resource_bundle.h"
+#include "base/logging.h"
+#import "base/mac_util.h"
+#include "base/string16.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/search_engines/template_url.h"
+#include "grit/app_resources.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+namespace {
+
+void ShiftOriginY(NSView* view, CGFloat amount) {
+ NSPoint origin = [view frame].origin;
+ origin.y += amount;
+ [view setFrameOrigin:origin];
+}
+
+} // namespace
+
+@implementation EditSearchEngineCocoaController
+
+- (id)initWithProfile:(Profile*)profile
+ delegate:(EditSearchEngineControllerDelegate*)delegate
+ templateURL:(const TemplateURL*)url {
+ DCHECK(profile);
+ NSString* nibpath = [mac_util::MainAppBundle()
+ pathForResource:@"EditSearchEngine"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ profile_ = profile;
+ templateURL_ = url;
+ controller_.reset(
+ new EditSearchEngineController(templateURL_, delegate, profile_));
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ DCHECK([self window]);
+ DCHECK_EQ(self, [[self window] delegate]);
+
+ // Make sure the url description field fits the text in it.
+ CGFloat descriptionShift = [GTMUILocalizerAndLayoutTweaker
+ sizeToFitFixedWidthTextField:urlDescriptionField_];
+
+ // Move the label container above the url description.
+ ShiftOriginY(labelContainer_, descriptionShift);
+ // There was no way via view containment to use a helper view to move all
+ // the textfields and images at once, most move them all on their own so
+ // they stay above the url description.
+ ShiftOriginY(nameField_, descriptionShift);
+ ShiftOriginY(keywordField_, descriptionShift);
+ ShiftOriginY(urlField_, descriptionShift);
+ ShiftOriginY(nameImage_, descriptionShift);
+ ShiftOriginY(keywordImage_, descriptionShift);
+ ShiftOriginY(urlImage_, descriptionShift);
+
+ // Resize the containing box for the name/keyword/url fields/images since it
+ // also contains the url description (which just grew).
+ [[fieldAndImageContainer_ contentView] setAutoresizesSubviews:NO];
+ NSRect rect = [fieldAndImageContainer_ frame];
+ rect.size.height += descriptionShift;
+ [fieldAndImageContainer_ setFrame:rect];
+ [[fieldAndImageContainer_ contentView] setAutoresizesSubviews:YES];
+
+ // Resize the window.
+ NSWindow* window = [self window];
+ NSSize windowDelta = NSMakeSize(0, descriptionShift);
+ [GTMUILocalizerAndLayoutTweaker
+ resizeWindowWithoutAutoResizingSubViews:window
+ delta:windowDelta];
+
+ ResourceBundle& bundle = ResourceBundle::GetSharedInstance();
+ goodImage_.reset([bundle.GetNativeImageNamed(IDR_INPUT_GOOD) retain]);
+ badImage_.reset([bundle.GetNativeImageNamed(IDR_INPUT_ALERT) retain]);
+ if (templateURL_) {
+ // Defaults to |..._NEW_WINDOW_TITLE|.
+ [window setTitle:l10n_util::GetNSString(
+ IDS_SEARCH_ENGINES_EDITOR_EDIT_WINDOW_TITLE)];
+ [nameField_ setStringValue:
+ base::SysWideToNSString(templateURL_->short_name())];
+ [keywordField_ setStringValue:
+ base::SysWideToNSString(templateURL_->keyword())];
+ [urlField_ setStringValue:
+ base::SysWideToNSString(templateURL_->url()->DisplayURL())];
+ [urlField_ setEnabled:(templateURL_->prepopulate_id() == 0)];
+ }
+ // When creating a new keyword, this will mark the fields as "invalid" and
+ // will not let the user save. If this is an edit, then this will set all
+ // the images to the "valid" state.
+ [self validateFields];
+}
+
+// When the window closes, clean ourselves up.
+- (void)windowWillClose:(NSNotification*)notif {
+ [self autorelease];
+}
+
+// Performs the logic of closing the window. If we are a sheet, then it ends the
+// modal session; otherwise, it closes the window.
+- (void)doClose {
+ if ([[self window] isSheet]) {
+ [NSApp endSheet:[self window]];
+ } else {
+ [[self window] close];
+ }
+}
+
+- (IBAction)cancel:(id)sender {
+ [self doClose];
+}
+
+- (IBAction)save:(id)sender {
+ DCHECK([self validateFields]);
+ string16 title = base::SysNSStringToUTF16([nameField_ stringValue]);
+ string16 keyword = base::SysNSStringToUTF16([keywordField_ stringValue]);
+ std::string url = base::SysNSStringToUTF8([urlField_ stringValue]);
+ controller_->AcceptAddOrEdit(title, keyword, url);
+ [self doClose];
+}
+
+// Delegate method for the text fields.
+
+- (void)controlTextDidChange:(NSNotification*)notif {
+ [self validateFields];
+}
+
+- (void)controlTextDidEndEditing:(NSNotification*)notif {
+ [self validateFields];
+}
+
+// Private --------------------------------------------------------------------
+
+// Sets the appropriate image and tooltip based on a boolean |valid|.
+- (void)setIsValid:(BOOL)valid
+ toolTip:(int)messageID
+ forImageView:(NSImageView*)imageView
+ textField:(NSTextField*)textField {
+ NSImage* image = (valid) ? goodImage_ : badImage_;
+ [imageView setImage:image];
+
+ NSString* toolTip = nil;
+ if (!valid)
+ toolTip = l10n_util::GetNSString(messageID);
+ [textField setToolTip:toolTip];
+ [imageView setToolTip:toolTip];
+}
+
+// This sets the image state for all the controls and enables or disables the
+// done button. Returns YES if all the fields are valid.
+- (BOOL)validateFields {
+ string16 title = base::SysNSStringToUTF16([nameField_ stringValue]);
+ BOOL titleValid = controller_->IsTitleValid(title);
+ [self setIsValid:titleValid
+ toolTip:IDS_SEARCH_ENGINES_INVALID_TITLE_TT
+ forImageView:nameImage_
+ textField:nameField_];
+
+ string16 keyword = base::SysNSStringToUTF16([keywordField_ stringValue]);
+ BOOL keywordValid = controller_->IsKeywordValid(keyword);
+ [self setIsValid:keywordValid
+ toolTip:IDS_SEARCH_ENGINES_INVALID_KEYWORD_TT
+ forImageView:keywordImage_
+ textField:keywordField_];
+
+ std::string url = base::SysNSStringToUTF8([urlField_ stringValue]);
+ BOOL urlValid = controller_->IsURLValid(url);
+ [self setIsValid:urlValid
+ toolTip:IDS_SEARCH_ENGINES_INVALID_URL_TT
+ forImageView:urlImage_
+ textField:urlField_];
+
+ BOOL isValid = (titleValid && keywordValid && urlValid);
+ [doneButton_ setEnabled:isValid];
+ return isValid;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/edit_search_engine_cocoa_controller_unittest.mm b/chrome/browser/ui/cocoa/edit_search_engine_cocoa_controller_unittest.mm
new file mode 100644
index 0000000..72bfe7b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/edit_search_engine_cocoa_controller_unittest.mm
@@ -0,0 +1,233 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "app/l10n_util_mac.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/search_engines/template_url.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/edit_search_engine_cocoa_controller.h"
+#include "chrome/test/testing_profile.h"
+#include "grit/generated_resources.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+@interface FakeEditSearchEngineController : EditSearchEngineCocoaController {
+}
+@property (nonatomic, readonly) NSTextField* nameField;
+@property (nonatomic, readonly) NSTextField* keywordField;
+@property (nonatomic, readonly) NSTextField* urlField;
+@property (nonatomic, readonly) NSImageView* nameImage;
+@property (nonatomic, readonly) NSImageView* keywordImage;
+@property (nonatomic, readonly) NSImageView* urlImage;
+@property (nonatomic, readonly) NSButton* doneButton;
+@property (nonatomic, readonly) NSImage* goodImage;
+@property (nonatomic, readonly) NSImage* badImage;
+@end
+
+@implementation FakeEditSearchEngineController
+@synthesize nameField = nameField_;
+@synthesize keywordField = keywordField_;
+@synthesize urlField = urlField_;
+@synthesize nameImage = nameImage_;
+@synthesize keywordImage = keywordImage_;
+@synthesize urlImage = urlImage_;
+@synthesize doneButton = doneButton_;
+- (NSImage*)goodImage {
+ return goodImage_.get();
+}
+- (NSImage*)badImage {
+ return badImage_.get();
+}
+@end
+
+namespace {
+
+class EditSearchEngineControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ TestingProfile* profile =
+ static_cast<TestingProfile*>(browser_helper_.profile());
+ profile->CreateTemplateURLModel();
+ controller_ = [[FakeEditSearchEngineController alloc]
+ initWithProfile:profile
+ delegate:nil
+ templateURL:nil];
+ }
+
+ virtual void TearDown() {
+ // Force the window to load so we hit |-awakeFromNib| to register as the
+ // window's delegate so that the controller can clean itself up in
+ // |-windowWillClose:|.
+ ASSERT_TRUE([controller_ window]);
+
+ [controller_ close];
+ CocoaTest::TearDown();
+ }
+
+ BrowserTestHelper browser_helper_;
+ FakeEditSearchEngineController* controller_;
+};
+
+TEST_F(EditSearchEngineControllerTest, ValidImageOriginals) {
+ EXPECT_FALSE([controller_ goodImage]);
+ EXPECT_FALSE([controller_ badImage]);
+
+ EXPECT_TRUE([controller_ window]); // Force the window to load.
+
+ EXPECT_TRUE([[controller_ goodImage] isKindOfClass:[NSImage class]]);
+ EXPECT_TRUE([[controller_ badImage] isKindOfClass:[NSImage class]]);
+
+ // Test window title is set correctly.
+ NSString* title = l10n_util::GetNSString(
+ IDS_SEARCH_ENGINES_EDITOR_NEW_WINDOW_TITLE);
+ EXPECT_NSEQ(title, [[controller_ window] title]);
+}
+
+TEST_F(EditSearchEngineControllerTest, SetImageViews) {
+ EXPECT_TRUE([controller_ window]); // Force the window to load.
+ EXPECT_EQ([controller_ badImage], [[controller_ nameImage] image]);
+ // An empty keyword is not OK.
+ EXPECT_EQ([controller_ badImage], [[controller_ keywordImage] image]);
+ EXPECT_EQ([controller_ badImage], [[controller_ urlImage] image]);
+}
+
+// This test ensures that on creating a new keyword, we are in an "invalid"
+// state that cannot save.
+TEST_F(EditSearchEngineControllerTest, InvalidState) {
+ EXPECT_TRUE([controller_ window]); // Force window to load.
+ NSString* toolTip = nil;
+ EXPECT_FALSE([controller_ validateFields]);
+
+ EXPECT_NSEQ(@"", [[controller_ nameField] stringValue]);
+ EXPECT_EQ([controller_ badImage], [[controller_ nameImage] image]);
+ toolTip = l10n_util::GetNSString(IDS_SEARCH_ENGINES_INVALID_TITLE_TT);
+ EXPECT_NSEQ(toolTip, [[controller_ nameField] toolTip]);
+ EXPECT_NSEQ(toolTip, [[controller_ nameImage] toolTip]);
+
+ // Keywords can not be empty strings.
+ EXPECT_NSEQ(@"", [[controller_ keywordField] stringValue]);
+ EXPECT_EQ([controller_ badImage], [[controller_ keywordImage] image]);
+ EXPECT_TRUE([[controller_ keywordField] toolTip]);
+ EXPECT_TRUE([[controller_ keywordImage] toolTip]);
+
+ EXPECT_NSEQ(@"", [[controller_ urlField] stringValue]);
+ EXPECT_EQ([controller_ badImage], [[controller_ urlImage] image]);
+ toolTip = l10n_util::GetNSString(IDS_SEARCH_ENGINES_INVALID_URL_TT);
+ EXPECT_NSEQ(toolTip, [[controller_ urlField] toolTip]);
+ EXPECT_NSEQ(toolTip, [[controller_ urlImage] toolTip]);
+}
+
+// Tests that the single name field validates.
+TEST_F(EditSearchEngineControllerTest, ValidateName) {
+ EXPECT_TRUE([controller_ window]); // Force window to load.
+
+ EXPECT_EQ([controller_ badImage], [[controller_ nameImage] image]);
+ EXPECT_FALSE([controller_ validateFields]);
+ NSString* toolTip =
+ l10n_util::GetNSString(IDS_SEARCH_ENGINES_INVALID_TITLE_TT);
+ EXPECT_NSEQ(toolTip, [[controller_ nameField] toolTip]);
+ EXPECT_NSEQ(toolTip, [[controller_ nameImage] toolTip]);
+ [[controller_ nameField] setStringValue:@"Test Name"];
+ EXPECT_FALSE([controller_ validateFields]);
+ EXPECT_EQ([controller_ goodImage], [[controller_ nameImage] image]);
+ EXPECT_FALSE([[controller_ nameField] toolTip]);
+ EXPECT_FALSE([[controller_ nameImage] toolTip]);
+ EXPECT_FALSE([[controller_ doneButton] isEnabled]);
+}
+
+// The keyword field is not valid if it is empty.
+TEST_F(EditSearchEngineControllerTest, ValidateKeyword) {
+ EXPECT_TRUE([controller_ window]); // Force window load.
+
+ EXPECT_EQ([controller_ badImage], [[controller_ keywordImage] image]);
+ EXPECT_FALSE([controller_ validateFields]);
+ EXPECT_TRUE([[controller_ keywordField] toolTip]);
+ EXPECT_TRUE([[controller_ keywordImage] toolTip]);
+ [[controller_ keywordField] setStringValue:@"foobar"];
+ EXPECT_FALSE([controller_ validateFields]);
+ EXPECT_EQ([controller_ goodImage], [[controller_ keywordImage] image]);
+ EXPECT_FALSE([[controller_ keywordField] toolTip]);
+ EXPECT_FALSE([[controller_ keywordImage] toolTip]);
+ EXPECT_FALSE([[controller_ doneButton] isEnabled]);
+}
+
+// Tests that the URL field validates.
+TEST_F(EditSearchEngineControllerTest, ValidateURL) {
+ EXPECT_TRUE([controller_ window]); // Force window to load.
+
+ EXPECT_EQ([controller_ badImage], [[controller_ urlImage] image]);
+ EXPECT_FALSE([controller_ validateFields]);
+ NSString* toolTip =
+ l10n_util::GetNSString(IDS_SEARCH_ENGINES_INVALID_URL_TT);
+ EXPECT_NSEQ(toolTip, [[controller_ urlField] toolTip]);
+ EXPECT_NSEQ(toolTip, [[controller_ urlImage] toolTip]);
+ [[controller_ urlField] setStringValue:@"http://foo-bar.com"];
+ EXPECT_FALSE([controller_ validateFields]);
+ EXPECT_EQ([controller_ goodImage], [[controller_ urlImage] image]);
+ EXPECT_FALSE([[controller_ urlField] toolTip]);
+ EXPECT_FALSE([[controller_ urlImage] toolTip]);
+ EXPECT_FALSE([[controller_ doneButton] isEnabled]);
+}
+
+// Tests that if the user enters all valid data that the UI reflects that
+// and that they can save.
+TEST_F(EditSearchEngineControllerTest, ValidateFields) {
+ EXPECT_TRUE([controller_ window]); // Force window to load.
+
+ // State before entering data.
+ EXPECT_EQ([controller_ badImage], [[controller_ nameImage] image]);
+ EXPECT_EQ([controller_ badImage], [[controller_ keywordImage] image]);
+ EXPECT_EQ([controller_ badImage], [[controller_ urlImage] image]);
+ EXPECT_FALSE([[controller_ doneButton] isEnabled]);
+ EXPECT_FALSE([controller_ validateFields]);
+
+ [[controller_ nameField] setStringValue:@"Test Name"];
+ EXPECT_FALSE([controller_ validateFields]);
+ EXPECT_EQ([controller_ goodImage], [[controller_ nameImage] image]);
+ EXPECT_FALSE([[controller_ doneButton] isEnabled]);
+
+ [[controller_ keywordField] setStringValue:@"foobar"];
+ EXPECT_FALSE([controller_ validateFields]);
+ EXPECT_EQ([controller_ goodImage], [[controller_ keywordImage] image]);
+ EXPECT_FALSE([[controller_ doneButton] isEnabled]);
+
+ // Once the URL is entered, we should have all 3 valid fields.
+ [[controller_ urlField] setStringValue:@"http://foo-bar.com"];
+ EXPECT_TRUE([controller_ validateFields]);
+ EXPECT_EQ([controller_ goodImage], [[controller_ urlImage] image]);
+ EXPECT_TRUE([[controller_ doneButton] isEnabled]);
+}
+
+// Tests editing an existing TemplateURL.
+TEST_F(EditSearchEngineControllerTest, EditTemplateURL) {
+ TemplateURL url;
+ url.set_short_name(L"Foobar");
+ url.set_keyword(L"keyword");
+ std::string urlString = TemplateURLRef::DisplayURLToURLRef(
+ L"http://foo-bar.com");
+ url.SetURL(urlString, 0, 1);
+ TestingProfile* profile = browser_helper_.profile();
+ FakeEditSearchEngineController *controller =
+ [[FakeEditSearchEngineController alloc] initWithProfile:profile
+ delegate:nil
+ templateURL:&url];
+ EXPECT_TRUE([controller window]);
+ NSString* title = l10n_util::GetNSString(
+ IDS_SEARCH_ENGINES_EDITOR_EDIT_WINDOW_TITLE);
+ EXPECT_NSEQ(title, [[controller window] title]);
+ NSString* nameString = [[controller nameField] stringValue];
+ EXPECT_NSEQ(@"Foobar", nameString);
+ NSString* keywordString = [[controller keywordField] stringValue];
+ EXPECT_NSEQ(@"keyword", keywordString);
+ NSString* urlValueString = [[controller urlField] stringValue];
+ EXPECT_NSEQ(@"http://foo-bar.com", urlValueString);
+ EXPECT_TRUE([controller validateFields]);
+ [controller close];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/encoding_menu_controller_delegate_mac.h b/chrome/browser/ui/cocoa/encoding_menu_controller_delegate_mac.h
new file mode 100644
index 0000000..989105c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/encoding_menu_controller_delegate_mac.h
@@ -0,0 +1,24 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_ENCODING_MENU_CONTROLLER_DELEGATE_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_ENCODING_MENU_CONTROLLER_DELEGATE_MAC_H_
+#pragma once
+
+#include "base/basictypes.h" // For DISALLOW_IMPLICIT_CONSTRUCTORS
+
+@class NSMenu;
+class Profile;
+
+// The Windows version of this class manages the Encoding Menu, but since Cocoa
+// does that for us automagically, the only thing left to do is construct
+// the encoding menu.
+class EncodingMenuControllerDelegate {
+ public:
+ static void BuildEncodingMenu(Profile *profile, NSMenu* encoding_menu);
+ private:
+ DISALLOW_IMPLICIT_CONSTRUCTORS(EncodingMenuControllerDelegate);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_ENCODING_MENU_CONTROLLER_DELEGATE_MAC_H_
diff --git a/chrome/browser/ui/cocoa/encoding_menu_controller_delegate_mac.mm b/chrome/browser/ui/cocoa/encoding_menu_controller_delegate_mac.mm
new file mode 100644
index 0000000..9045540
--- /dev/null
+++ b/chrome/browser/ui/cocoa/encoding_menu_controller_delegate_mac.mm
@@ -0,0 +1,60 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/encoding_menu_controller_delegate_mac.h"
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/string16.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/encoding_menu_controller.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/ui/browser.h"
+
+namespace {
+
+void AddSeparatorToMenu(NSMenu *parent_menu) {
+ NSMenuItem* separator = [NSMenuItem separatorItem];
+ [parent_menu addItem:separator];
+}
+
+void AppendMenuItem(NSMenu *parent_menu, int tag, NSString *title) {
+
+ NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:title
+ action:nil
+ keyEquivalent:@""] autorelease];
+ [parent_menu addItem:item];
+ [item setAction:@selector(commandDispatch:)];
+ [item setTag:tag];
+}
+
+} // namespace
+
+// static
+void EncodingMenuControllerDelegate::BuildEncodingMenu(Profile *profile,
+ NSMenu* encoding_menu) {
+ DCHECK(profile);
+
+ typedef EncodingMenuController::EncodingMenuItemList EncodingMenuItemList;
+ EncodingMenuItemList menuItems;
+ EncodingMenuController controller;
+ controller.GetEncodingMenuItems(profile, &menuItems);
+
+ for (EncodingMenuItemList::iterator it = menuItems.begin();
+ it != menuItems.end();
+ ++it) {
+ int item_id = it->first;
+ string16 &localized_title_string16 = it->second;
+
+ if (item_id == 0) {
+ AddSeparatorToMenu(encoding_menu);
+ } else {
+ NSString *localized_title =
+ base::SysUTF16ToNSString(localized_title_string16);
+ AppendMenuItem(encoding_menu, item_id, localized_title);
+ }
+ }
+
+}
diff --git a/chrome/browser/ui/cocoa/event_utils.h b/chrome/browser/ui/cocoa/event_utils.h
new file mode 100644
index 0000000..a8f24f8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/event_utils.h
@@ -0,0 +1,30 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_EVENT_UTILS_H_
+#define CHROME_BROWSER_UI_COCOA_EVENT_UTILS_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "webkit/glue/window_open_disposition.h"
+
+namespace event_utils {
+
+// Retrieves the WindowOpenDisposition used to open a link from a user gesture
+// represented by |event|. For example, a Cmd+Click would mean open the
+// associated link in a background tab.
+WindowOpenDisposition WindowOpenDispositionFromNSEvent(NSEvent* event);
+
+// Retrieves the WindowOpenDisposition used to open a link from a user gesture
+// represented by |event|, but instead use the modifier flags given by |flags|,
+// which is the same format as |-NSEvent modifierFlags|. This allows
+// substitution of the modifiers without having to create a new event from
+// scratch.
+WindowOpenDisposition WindowOpenDispositionFromNSEventWithFlags(
+ NSEvent* event, NSUInteger flags);
+
+} // namespace event_utils
+
+#endif // CHROME_BROWSER_UI_COCOA_EVENT_UTILS_H_
diff --git a/chrome/browser/ui/cocoa/event_utils.mm b/chrome/browser/ui/cocoa/event_utils.mm
new file mode 100644
index 0000000..ec475b5
--- /dev/null
+++ b/chrome/browser/ui/cocoa/event_utils.mm
@@ -0,0 +1,21 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/event_utils.h"
+
+namespace event_utils {
+
+WindowOpenDisposition WindowOpenDispositionFromNSEvent(NSEvent* event) {
+ NSUInteger modifiers = [event modifierFlags];
+ return WindowOpenDispositionFromNSEventWithFlags(event, modifiers);
+}
+
+WindowOpenDisposition WindowOpenDispositionFromNSEventWithFlags(
+ NSEvent* event, NSUInteger flags) {
+ if ([event buttonNumber] == 2 || flags & NSCommandKeyMask)
+ return flags & NSShiftKeyMask ? NEW_FOREGROUND_TAB : NEW_BACKGROUND_TAB;
+ return flags & NSShiftKeyMask ? NEW_WINDOW : CURRENT_TAB;
+}
+
+} // namespace event_utils
diff --git a/chrome/browser/ui/cocoa/event_utils_unittest.mm b/chrome/browser/ui/cocoa/event_utils_unittest.mm
new file mode 100644
index 0000000..6f0b7fe
--- /dev/null
+++ b/chrome/browser/ui/cocoa/event_utils_unittest.mm
@@ -0,0 +1,61 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <objc/objc-class.h>
+
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/event_utils.h"
+#include "chrome/browser/ui/cocoa/test_event_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+// We provide a donor class with a specially modified |modifierFlags|
+// implementation that we swap with NSEvent's. This is because we can't create a
+// NSEvent that represents a middle click with modifiers.
+@interface TestEvent : NSObject
+@end
+@implementation TestEvent
+- (NSUInteger)modifierFlags { return NSShiftKeyMask; }
+@end
+
+namespace {
+
+class EventUtilsTest : public CocoaTest {
+};
+
+TEST_F(EventUtilsTest, TestWindowOpenDispositionFromNSEvent) {
+ // Left Click = same tab.
+ NSEvent* me = test_event_utils::MakeMouseEvent(NSLeftMouseUp, 0);
+ EXPECT_EQ(CURRENT_TAB, event_utils::WindowOpenDispositionFromNSEvent(me));
+
+ // Middle Click = new background tab.
+ me = test_event_utils::MakeMouseEvent(NSOtherMouseUp, 0);
+ EXPECT_EQ(NEW_BACKGROUND_TAB,
+ event_utils::WindowOpenDispositionFromNSEvent(me));
+
+ // Shift+Middle Click = new foreground tab.
+ {
+ ScopedClassSwizzler swizzler([NSEvent class], [TestEvent class],
+ @selector(modifierFlags));
+ me = test_event_utils::MakeMouseEvent(NSOtherMouseUp, NSShiftKeyMask);
+ EXPECT_EQ(NEW_FOREGROUND_TAB,
+ event_utils::WindowOpenDispositionFromNSEvent(me));
+ }
+
+ // Cmd+Left Click = new background tab.
+ me = test_event_utils::MakeMouseEvent(NSLeftMouseUp, NSCommandKeyMask);
+ EXPECT_EQ(NEW_BACKGROUND_TAB,
+ event_utils::WindowOpenDispositionFromNSEvent(me));
+
+ // Cmd+Shift+Left Click = new foreground tab.
+ me = test_event_utils::MakeMouseEvent(NSLeftMouseUp, NSCommandKeyMask | NSShiftKeyMask);
+ EXPECT_EQ(NEW_FOREGROUND_TAB,
+ event_utils::WindowOpenDispositionFromNSEvent(me));
+
+ // Shift+Left Click = new window
+ me = test_event_utils::MakeMouseEvent(NSLeftMouseUp, NSShiftKeyMask);
+ EXPECT_EQ(NEW_WINDOW, event_utils::WindowOpenDispositionFromNSEvent(me));
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/extension_install_prompt.mm b/chrome/browser/ui/cocoa/extension_install_prompt.mm
new file mode 100644
index 0000000..3306806
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extension_install_prompt.mm
@@ -0,0 +1,51 @@
+// 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 <string>
+
+#include "app/l10n_util_mac.h"
+#include "app/resource_bundle.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/extensions/extension_install_ui.h"
+#include "chrome/common/extensions/extension.h"
+#include "grit/browser_resources.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+
+class Profile;
+
+void ExtensionInstallUI::ShowExtensionInstallUIPromptImpl(
+ Profile* profile,
+ Delegate* delegate,
+ const Extension* extension,
+ SkBitmap* icon,
+ ExtensionInstallUI::PromptType type) {
+ NSAlert* alert = [[[NSAlert alloc] init] autorelease];
+
+ NSButton* continueButton = [alert addButtonWithTitle:l10n_util::GetNSString(
+ ExtensionInstallUI::kButtonIds[type])];
+ // Clear the key equivalent (currently 'Return') because cancel is the default
+ // button.
+ [continueButton setKeyEquivalent:@""];
+
+ NSButton* cancelButton = [alert addButtonWithTitle:l10n_util::GetNSString(
+ IDS_CANCEL)];
+ [cancelButton setKeyEquivalent:@"\r"];
+
+ [alert setMessageText:l10n_util::GetNSStringF(
+ ExtensionInstallUI::kHeadingIds[type],
+ UTF8ToUTF16(extension->name()))];
+ [alert setAlertStyle:NSWarningAlertStyle];
+ [alert setIcon:gfx::SkBitmapToNSImage(*icon)];
+
+ if ([alert runModal] == NSAlertFirstButtonReturn) {
+ delegate->InstallUIProceed();
+ } else {
+ delegate->InstallUIAbort();
+ }
+}
diff --git a/chrome/browser/ui/cocoa/extension_installed_bubble_bridge.h b/chrome/browser/ui/cocoa/extension_installed_bubble_bridge.h
new file mode 100644
index 0000000..62d72c3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extension_installed_bubble_bridge.h
@@ -0,0 +1,28 @@
+// 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.
+
+// C++ bridge function to connect ExtensionInstallUI to the Cocoa-based
+// extension installed bubble.
+
+#ifndef CHROME_BROWSER_UI_COCOA_EXTENSION_INSTALLED_BUBBLE_BRIDGE_H_
+#define CHROME_BROWSER_UI_COCOA_EXTENSION_INSTALLED_BUBBLE_BRIDGE_H_
+#pragma once
+
+#include "gfx/native_widget_types.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+class Browser;
+class Extension;
+
+namespace ExtensionInstalledBubbleCocoa {
+
+// This function is called by the ExtensionInstallUI when an extension has been
+// installed.
+void ShowExtensionInstalledBubble(gfx::NativeWindow window,
+ const Extension* extension,
+ Browser* browser,
+ SkBitmap icon);
+}
+
+#endif // CHROME_BROWSER_UI_COCOA_EXTENSION_INSTALLED_BUBBLE_BRIDGE_H_
diff --git a/chrome/browser/ui/cocoa/extension_installed_bubble_bridge.mm b/chrome/browser/ui/cocoa/extension_installed_bubble_bridge.mm
new file mode 100644
index 0000000..d439899
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extension_installed_bubble_bridge.mm
@@ -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 "extension_installed_bubble_bridge.h"
+
+#import "chrome/browser/ui/cocoa/extension_installed_bubble_controller.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/common/extensions/extension.h"
+
+void ExtensionInstalledBubbleCocoa::ShowExtensionInstalledBubble(
+ gfx::NativeWindow window,
+ const Extension* extension,
+ Browser* browser,
+ SkBitmap icon) {
+ // The controller is deallocated when the window is closed, so no need to
+ // worry about it here.
+ [[ExtensionInstalledBubbleController alloc]
+ initWithParentWindow:window
+ extension:extension
+ browser:browser
+ icon:icon];
+}
diff --git a/chrome/browser/ui/cocoa/extension_installed_bubble_controller.h b/chrome/browser/ui/cocoa/extension_installed_bubble_controller.h
new file mode 100644
index 0000000..3178d47
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extension_installed_bubble_controller.h
@@ -0,0 +1,112 @@
+// 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_UI_COCOA_EXTENSION_INSTALLED_BUBBLE_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_EXTENSION_INSTALLED_BUBBLE_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_ptr.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+class Browser;
+class Extension;
+class ExtensionLoadedNotificationObserver;
+@class HoverCloseButton;
+@class InfoBubbleView;
+
+namespace extension_installed_bubble {
+
+// Maximum height or width of extension's icon (corresponds to Windows & GTK).
+const int kIconSize = 43;
+
+// Outer vertical margin for text, icon, and closing x.
+const int kOuterVerticalMargin = 15;
+
+// Inner vertical margin for text messages.
+const int kInnerVerticalMargin = 10;
+
+// We use a different kind of notification for each of these extension types.
+typedef enum {
+ kBrowserAction,
+ kGeneric,
+ kOmniboxKeyword,
+ kPageAction
+} ExtensionType;
+
+}
+
+// Controller for the extension installed bubble. This bubble pops up after
+// an extension has been installed to inform the user that the install happened
+// properly, and to let the user know how to manage this extension in the
+// future.
+@interface ExtensionInstalledBubbleController :
+ NSWindowController<NSWindowDelegate> {
+ @private
+ NSWindow* parentWindow_; // weak
+ const Extension* extension_; // weak
+ Browser* browser_; // weak
+ scoped_nsobject<NSImage> icon_;
+
+ extension_installed_bubble::ExtensionType type_;
+
+ // We need to remove the page action immediately when the browser window
+ // closes while this bubble is still open, so the bubble's closing animation
+ // doesn't overlap browser destruction.
+ BOOL pageActionRemoved_;
+
+ // Lets us register for EXTENSION_LOADED notifications. The actual
+ // notifications are sent to the observer object, which proxies them
+ // back to the controller.
+ scoped_ptr<ExtensionLoadedNotificationObserver> extensionObserver_;
+
+ // References below are weak, being obtained from the nib.
+ IBOutlet InfoBubbleView* infoBubbleView_;
+ IBOutlet HoverCloseButton* closeButton_;
+ IBOutlet NSImageView* iconImage_;
+ IBOutlet NSTextField* extensionInstalledMsg_;
+ // Only shown for page actions and omnibox keywords.
+ IBOutlet NSTextField* extraInfoMsg_;
+ IBOutlet NSTextField* extensionInstalledInfoMsg_;
+}
+
+@property (nonatomic, readonly) const Extension* extension;
+@property (nonatomic) BOOL pageActionRemoved;
+
+// Initialize the window, and then create observers to wait for the extension
+// to complete loading, or the browser window to close.
+- (id)initWithParentWindow:(NSWindow*)parentWindow
+ extension:(const Extension*)extension
+ browser:(Browser*)browser
+ icon:(SkBitmap)icon;
+
+// Action for close button.
+- (IBAction)closeWindow:(id)sender;
+
+// Displays the extension installed bubble. This callback is triggered by
+// the extensionObserver when the extension has completed loading.
+- (void)showWindow:(id)sender;
+
+// Clears our weak pointer to the Extension. This callback is triggered by
+// the extensionObserver when the extension is unloaded.
+- (void)extensionUnloaded:(id)sender;
+
+@end
+
+@interface ExtensionInstalledBubbleController(ExposedForTesting)
+
+- (void)removePageActionPreviewIfNecessary;
+- (NSWindow*)initializeWindow;
+- (int)calculateWindowHeight;
+- (void)setMessageFrames:(int)newWindowHeight;
+- (NSRect)getExtensionInstalledMsgFrame;
+- (NSRect)getExtraInfoMsgFrame;
+- (NSRect)getExtensionInstalledInfoMsgFrame;
+
+@end // ExtensionInstalledBubbleController(ExposedForTesting)
+
+#endif // CHROME_BROWSER_UI_COCOA_EXTENSION_INSTALLED_BUBBLE_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/extension_installed_bubble_controller.mm b/chrome/browser/ui/cocoa/extension_installed_bubble_controller.mm
new file mode 100644
index 0000000..1744fb7
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extension_installed_bubble_controller.mm
@@ -0,0 +1,374 @@
+// 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 "extension_installed_bubble_controller.h"
+
+#include "app/l10n_util.h"
+#include "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/ui/cocoa/browser_window_cocoa.h"
+#include "chrome/browser/ui/cocoa/browser_window_controller.h"
+#include "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h"
+#include "chrome/browser/ui/cocoa/hover_close_button.h"
+#include "chrome/browser/ui/cocoa/info_bubble_view.h"
+#include "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
+#include "chrome/browser/ui/cocoa/toolbar_controller.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chrome/common/extensions/extension.h"
+#include "chrome/common/extensions/extension_action.h"
+#include "chrome/common/notification_registrar.h"
+#include "chrome/common/notification_service.h"
+#include "grit/generated_resources.h"
+#import "skia/ext/skia_utils_mac.h"
+#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+
+// C++ class that receives EXTENSION_LOADED notifications and proxies them back
+// to |controller|.
+class ExtensionLoadedNotificationObserver : public NotificationObserver {
+ public:
+ ExtensionLoadedNotificationObserver(
+ ExtensionInstalledBubbleController* controller, Profile* profile)
+ : controller_(controller) {
+ registrar_.Add(this, NotificationType::EXTENSION_LOADED,
+ Source<Profile>(profile));
+ registrar_.Add(this, NotificationType::EXTENSION_UNLOADED,
+ Source<Profile>(profile));
+ }
+
+ private:
+ // NotificationObserver implementation. Tells the controller to start showing
+ // its window on the main thread when the extension has finished loading.
+ void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ if (type == NotificationType::EXTENSION_LOADED) {
+ const Extension* extension = Details<const Extension>(details).ptr();
+ if (extension == [controller_ extension]) {
+ [controller_ performSelectorOnMainThread:@selector(showWindow:)
+ withObject:controller_
+ waitUntilDone:NO];
+ }
+ } else if (type == NotificationType::EXTENSION_UNLOADED) {
+ const Extension* extension = Details<const Extension>(details).ptr();
+ if (extension == [controller_ extension]) {
+ [controller_ performSelectorOnMainThread:@selector(extensionUnloaded:)
+ withObject:controller_
+ waitUntilDone:NO];
+ }
+ } else {
+ NOTREACHED() << "Received unexpected notification.";
+ }
+ }
+
+ NotificationRegistrar registrar_;
+ ExtensionInstalledBubbleController* controller_; // weak, owns us
+};
+
+@implementation ExtensionInstalledBubbleController
+
+@synthesize extension = extension_;
+@synthesize pageActionRemoved = pageActionRemoved_; // Exposed for unit test.
+
+- (id)initWithParentWindow:(NSWindow*)parentWindow
+ extension:(const Extension*)extension
+ browser:(Browser*)browser
+ icon:(SkBitmap)icon {
+ NSString* nibPath =
+ [mac_util::MainAppBundle() pathForResource:@"ExtensionInstalledBubble"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
+ DCHECK(parentWindow);
+ parentWindow_ = parentWindow;
+ DCHECK(extension);
+ extension_ = extension;
+ DCHECK(browser);
+ browser_ = browser;
+ icon_.reset([gfx::SkBitmapToNSImage(icon) retain]);
+ pageActionRemoved_ = NO;
+
+ if (!extension->omnibox_keyword().empty()) {
+ type_ = extension_installed_bubble::kOmniboxKeyword;
+ } else if (extension->browser_action()) {
+ type_ = extension_installed_bubble::kBrowserAction;
+ } else if (extension->page_action() &&
+ !extension->page_action()->default_icon_path().empty()) {
+ type_ = extension_installed_bubble::kPageAction;
+ } else {
+ NOTREACHED(); // kGeneric installs handled in the extension_install_ui.
+ }
+
+ // Start showing window only after extension has fully loaded.
+ extensionObserver_.reset(new ExtensionLoadedNotificationObserver(
+ self, browser->profile()));
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+- (void)close {
+ [parentWindow_ removeChildWindow:[self window]];
+ [super close];
+}
+
+- (void)windowWillClose:(NSNotification*)notification {
+ // Turn off page action icon preview when the window closes, unless we
+ // already removed it when the window resigned key status.
+ [self removePageActionPreviewIfNecessary];
+ extension_ = NULL;
+ browser_ = NULL;
+ parentWindow_ = nil;
+ // We caught a close so we don't need to watch for the parent closing.
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [self autorelease];
+}
+
+// The controller is the delegate of the window, so it receives "did resign
+// key" notifications. When key is resigned, close the window.
+- (void)windowDidResignKey:(NSNotification*)notification {
+ NSWindow* window = [self window];
+ DCHECK_EQ([notification object], window);
+ DCHECK([window isVisible]);
+
+ // If the browser window is closing, we need to remove the page action
+ // immediately, otherwise the closing animation may overlap with
+ // browser destruction.
+ [self removePageActionPreviewIfNecessary];
+ [self close];
+}
+
+- (IBAction)closeWindow:(id)sender {
+ DCHECK([[self window] isVisible]);
+ [self close];
+}
+
+// Extracted to a function here so that it can be overwritten for unit
+// testing.
+- (void)removePageActionPreviewIfNecessary {
+ if (!extension_ || !extension_->page_action() || pageActionRemoved_)
+ return;
+ pageActionRemoved_ = YES;
+
+ BrowserWindowCocoa* window =
+ static_cast<BrowserWindowCocoa*>(browser_->window());
+ LocationBarViewMac* locationBarView =
+ [window->cocoa_controller() locationBarBridge];
+ locationBarView->SetPreviewEnabledPageAction(extension_->page_action(),
+ false); // disables preview.
+}
+
+// The extension installed bubble points at the browser action icon or the
+// page action icon (shown as a preview), depending on the extension type.
+// We need to calculate the location of these icons and the size of the
+// message itself (which varies with the title of the extension) in order
+// to figure out the origin point for the extension installed bubble.
+// TODO(mirandac): add framework to easily test extension UI components!
+- (NSPoint)calculateArrowPoint {
+ BrowserWindowCocoa* window =
+ static_cast<BrowserWindowCocoa*>(browser_->window());
+ NSPoint arrowPoint = NSZeroPoint;
+
+ switch(type_) {
+ case extension_installed_bubble::kOmniboxKeyword: {
+ LocationBarViewMac* locationBarView =
+ [window->cocoa_controller() locationBarBridge];
+ arrowPoint = locationBarView->GetPageInfoBubblePoint();
+ break;
+ }
+ case extension_installed_bubble::kBrowserAction: {
+ BrowserActionsController* controller =
+ [[window->cocoa_controller() toolbarController]
+ browserActionsController];
+ arrowPoint = [controller popupPointForBrowserAction:extension_];
+ break;
+ }
+ case extension_installed_bubble::kPageAction: {
+ LocationBarViewMac* locationBarView =
+ [window->cocoa_controller() locationBarBridge];
+
+ // Tell the location bar to show a preview of the page action icon, which
+ // would ordinarily only be displayed on a page of the appropriate type.
+ // We remove this preview when the extension installed bubble closes.
+ locationBarView->SetPreviewEnabledPageAction(extension_->page_action(),
+ true);
+
+ // Find the center of the bottom of the page action icon.
+ arrowPoint =
+ locationBarView->GetPageActionBubblePoint(extension_->page_action());
+ break;
+ }
+ default: {
+ NOTREACHED() << "Generic extension type not allowed in install bubble.";
+ }
+ }
+ return arrowPoint;
+}
+
+// We want this to be a child of a browser window. addChildWindow:
+// (called from this function) will bring the window on-screen;
+// unfortunately, [NSWindowController showWindow:] will also bring it
+// on-screen (but will cause unexpected changes to the window's
+// position). We cannot have an addChildWindow: and a subsequent
+// showWindow:. Thus, we have our own version.
+- (void)showWindow:(id)sender {
+ // Generic extensions get an infobar rather than a bubble.
+ DCHECK(type_ != extension_installed_bubble::kGeneric);
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+
+ // Load nib and calculate height based on messages to be shown.
+ NSWindow* window = [self initializeWindow];
+ int newWindowHeight = [self calculateWindowHeight];
+ [infoBubbleView_ setFrameSize:NSMakeSize(
+ NSWidth([[window contentView] bounds]), newWindowHeight)];
+ NSSize windowDelta = NSMakeSize(
+ 0, newWindowHeight - NSHeight([[window contentView] bounds]));
+ windowDelta = [[window contentView] convertSize:windowDelta toView:nil];
+ NSRect newFrame = [window frame];
+ newFrame.size.height += windowDelta.height;
+ [window setFrame:newFrame display:NO];
+
+ // Now that we have resized the window, adjust y pos of the messages.
+ [self setMessageFrames:newWindowHeight];
+
+ // Find window origin, taking into account bubble size and arrow location.
+ NSPoint origin =
+ [parentWindow_ convertBaseToScreen:[self calculateArrowPoint]];
+ NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
+ info_bubble::kBubbleArrowWidth / 2.0, 0);
+ offsets = [[window contentView] convertSize:offsets toView:nil];
+ if ([infoBubbleView_ arrowLocation] == info_bubble::kTopRight)
+ origin.x -= NSWidth([window frame]) - offsets.width;
+ origin.y -= NSHeight([window frame]);
+ [window setFrameOrigin:origin];
+
+ [parentWindow_ addChildWindow:window
+ ordered:NSWindowAbove];
+ [window makeKeyAndOrderFront:self];
+}
+
+// Finish nib loading, set arrow location and load icon into window. This
+// function is exposed for unit testing.
+- (NSWindow*)initializeWindow {
+ NSWindow* window = [self window]; // completes nib load
+
+ if (type_ == extension_installed_bubble::kOmniboxKeyword) {
+ [infoBubbleView_ setArrowLocation:info_bubble::kTopLeft];
+ } else {
+ [infoBubbleView_ setArrowLocation:info_bubble::kTopRight];
+ }
+
+ // Set appropriate icon, resizing if necessary.
+ if ([icon_ size].width > extension_installed_bubble::kIconSize) {
+ [icon_ setSize:NSMakeSize(extension_installed_bubble::kIconSize,
+ extension_installed_bubble::kIconSize)];
+ }
+ [iconImage_ setImage:icon_];
+ [iconImage_ setNeedsDisplay:YES];
+ return window;
+ }
+
+// Calculate the height of each install message, resizing messages in their
+// frames to fit window width. Return the new window height, based on the
+// total of all message heights.
+- (int)calculateWindowHeight {
+ // Adjust the window height to reflect the sum height of all messages
+ // and vertical padding.
+ int newWindowHeight = 2 * extension_installed_bubble::kOuterVerticalMargin;
+
+ // First part of extension installed message.
+ [extensionInstalledMsg_ setStringValue:l10n_util::GetNSStringF(
+ IDS_EXTENSION_INSTALLED_HEADING, UTF8ToUTF16(extension_->name()))];
+ [GTMUILocalizerAndLayoutTweaker
+ sizeToFitFixedWidthTextField:extensionInstalledMsg_];
+ newWindowHeight += [extensionInstalledMsg_ frame].size.height +
+ extension_installed_bubble::kInnerVerticalMargin;
+
+ // If type is page action, include a special message about page actions.
+ if (type_ == extension_installed_bubble::kPageAction) {
+ [extraInfoMsg_ setHidden:NO];
+ [[extraInfoMsg_ cell]
+ setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+ [GTMUILocalizerAndLayoutTweaker
+ sizeToFitFixedWidthTextField:extraInfoMsg_];
+ newWindowHeight += [extraInfoMsg_ frame].size.height +
+ extension_installed_bubble::kInnerVerticalMargin;
+ }
+
+ // If type is omnibox keyword, include a special message about the keyword.
+ if (type_ == extension_installed_bubble::kOmniboxKeyword) {
+ [extraInfoMsg_ setStringValue:l10n_util::GetNSStringF(
+ IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO,
+ UTF8ToUTF16(extension_->omnibox_keyword()))];
+ [extraInfoMsg_ setHidden:NO];
+ [[extraInfoMsg_ cell]
+ setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+ [GTMUILocalizerAndLayoutTweaker
+ sizeToFitFixedWidthTextField:extraInfoMsg_];
+ newWindowHeight += [extraInfoMsg_ frame].size.height +
+ extension_installed_bubble::kInnerVerticalMargin;
+ }
+
+ // Second part of extension installed message.
+ [[extensionInstalledInfoMsg_ cell]
+ setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+ [GTMUILocalizerAndLayoutTweaker
+ sizeToFitFixedWidthTextField:extensionInstalledInfoMsg_];
+ newWindowHeight += [extensionInstalledInfoMsg_ frame].size.height;
+
+ return newWindowHeight;
+}
+
+// Adjust y-position of messages to sit properly in new window height.
+- (void)setMessageFrames:(int)newWindowHeight {
+ // The extension messages will always be shown.
+ NSRect extensionMessageFrame1 = [extensionInstalledMsg_ frame];
+ NSRect extensionMessageFrame2 = [extensionInstalledInfoMsg_ frame];
+
+ extensionMessageFrame1.origin.y = newWindowHeight - (
+ extensionMessageFrame1.size.height +
+ extension_installed_bubble::kOuterVerticalMargin);
+ [extensionInstalledMsg_ setFrame:extensionMessageFrame1];
+ if (type_ == extension_installed_bubble::kPageAction ||
+ type_ == extension_installed_bubble::kOmniboxKeyword) {
+ // The extra message is only shown when appropriate.
+ NSRect extraMessageFrame = [extraInfoMsg_ frame];
+ extraMessageFrame.origin.y = extensionMessageFrame1.origin.y - (
+ extraMessageFrame.size.height +
+ extension_installed_bubble::kInnerVerticalMargin);
+ [extraInfoMsg_ setFrame:extraMessageFrame];
+ extensionMessageFrame2.origin.y = extraMessageFrame.origin.y - (
+ extensionMessageFrame2.size.height +
+ extension_installed_bubble::kInnerVerticalMargin);
+ } else {
+ extensionMessageFrame2.origin.y = extensionMessageFrame1.origin.y - (
+ extensionMessageFrame2.size.height +
+ extension_installed_bubble::kInnerVerticalMargin);
+ }
+ [extensionInstalledInfoMsg_ setFrame:extensionMessageFrame2];
+}
+
+// Exposed for unit testing.
+- (NSRect)getExtensionInstalledMsgFrame {
+ return [extensionInstalledMsg_ frame];
+}
+
+- (NSRect)getExtraInfoMsgFrame {
+ return [extraInfoMsg_ frame];
+}
+
+- (NSRect)getExtensionInstalledInfoMsgFrame {
+ return [extensionInstalledInfoMsg_ frame];
+}
+
+- (void)extensionUnloaded:(id)sender {
+ extension_ = NULL;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/extension_installed_bubble_controller_unittest.mm b/chrome/browser/ui/cocoa/extension_installed_bubble_controller_unittest.mm
new file mode 100644
index 0000000..f1171d1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extension_installed_bubble_controller_unittest.mm
@@ -0,0 +1,202 @@
+// 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/file_path.h"
+#include "base/file_util.h"
+#include "base/path_service.h"
+#include "base/scoped_ptr.h"
+#include "base/values.h"
+#import "chrome/browser/browser_window.h"
+#import "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/extension_installed_bubble_controller.h"
+#include "chrome/common/chrome_paths.h"
+#include "chrome/common/extensions/extension.h"
+#include "chrome/common/extensions/extension_constants.h"
+#include "webkit/glue/image_decoder.h"
+
+// ExtensionInstalledBubbleController with removePageActionPreview overridden
+// to a no-op, because pageActions are not yet hooked up in the test browser.
+@interface ExtensionInstalledBubbleControllerForTest :
+ ExtensionInstalledBubbleController {
+}
+
+// Do nothing, because browser window is not set up with page actions
+// for unit testing.
+- (void)removePageActionPreview;
+
+@end
+
+@implementation ExtensionInstalledBubbleControllerForTest
+
+- (void)removePageActionPreview { }
+
+@end
+
+namespace keys = extension_manifest_keys;
+
+class ExtensionInstalledBubbleControllerTest : public CocoaTest {
+
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ browser_ = helper_.browser();
+ window_ = helper_.CreateBrowserWindow()->GetNativeHandle();
+ icon_ = LoadTestIcon();
+ }
+
+ virtual void TearDown() {
+ helper_.CloseBrowserWindow();
+ CocoaTest::TearDown();
+ }
+
+ // Load test icon from extension test directory.
+ SkBitmap LoadTestIcon() {
+ FilePath path;
+ PathService::Get(chrome::DIR_TEST_DATA, &path);
+ path = path.AppendASCII("extensions").AppendASCII("icon1.png");
+
+ std::string file_contents;
+ file_util::ReadFileToString(path, &file_contents);
+ const unsigned char* data =
+ reinterpret_cast<const unsigned char*>(file_contents.data());
+
+ SkBitmap bitmap;
+ webkit_glue::ImageDecoder decoder;
+ bitmap = decoder.Decode(data, file_contents.length());
+
+ return bitmap;
+ }
+
+ // Create a skeletal framework of either page action or browser action
+ // type. This extension only needs to have a type and a name to initialize
+ // the ExtensionInstalledBubble for unit testing.
+ scoped_refptr<Extension> CreateExtension(
+ extension_installed_bubble::ExtensionType type) {
+ FilePath path;
+ PathService::Get(chrome::DIR_TEST_DATA, &path);
+ path = path.AppendASCII("extensions").AppendASCII("dummy");
+
+ DictionaryValue extension_input_value;
+ extension_input_value.SetString(keys::kVersion, "1.0.0.0");
+ if (type == extension_installed_bubble::kPageAction) {
+ extension_input_value.SetString(keys::kName, "page action extension");
+ DictionaryValue* action = new DictionaryValue;
+ action->SetString(keys::kPageActionId, "ExtensionActionId");
+ action->SetString(keys::kPageActionDefaultTitle, "ExtensionActionTitle");
+ action->SetString(keys::kPageActionDefaultIcon, "image1.png");
+ ListValue* action_list = new ListValue;
+ action_list->Append(action);
+ extension_input_value.Set(keys::kPageActions, action_list);
+ } else {
+ extension_input_value.SetString(keys::kName, "browser action extension");
+ DictionaryValue* browser_action = new DictionaryValue;
+ // An empty dictionary is enough to create a Browser Action.
+ extension_input_value.Set(keys::kBrowserAction, browser_action);
+ }
+
+ std::string error;
+ return Extension::Create(
+ path, Extension::INVALID, extension_input_value, false, &error);
+ }
+
+ // Allows us to create the window and browser for testing.
+ BrowserTestHelper helper_;
+
+ // Required to initialize the extension installed bubble.
+ NSWindow* window_; // weak, owned by BrowserTestHelper.
+
+ // Required to initialize the extension installed bubble.
+ Browser* browser_; // weak, owned by BrowserTestHelper.
+
+ // Skeleton extension to be tested; reinitialized for each test.
+ scoped_refptr<Extension> extension_;
+
+ // The icon_ to be loaded into the bubble window.
+ SkBitmap icon_;
+};
+
+// Confirm that window sizes are set correctly for a page action extension.
+TEST_F(ExtensionInstalledBubbleControllerTest, PageActionTest) {
+ extension_ = CreateExtension(extension_installed_bubble::kPageAction);
+ ExtensionInstalledBubbleControllerForTest* controller =
+ [[ExtensionInstalledBubbleControllerForTest alloc]
+ initWithParentWindow:window_
+ extension:extension_.get()
+ browser:browser_
+ icon:icon_];
+ EXPECT_TRUE(controller);
+
+ // Initialize window without having to calculate tabstrip locations.
+ [controller initializeWindow];
+ EXPECT_TRUE([controller window]);
+
+ int height = [controller calculateWindowHeight];
+ // Height should equal the vertical padding + height of all messages.
+ int correctHeight = 2 * extension_installed_bubble::kOuterVerticalMargin +
+ 2 * extension_installed_bubble::kInnerVerticalMargin +
+ [controller getExtensionInstalledMsgFrame].size.height +
+ [controller getExtensionInstalledInfoMsgFrame].size.height +
+ [controller getExtraInfoMsgFrame].size.height;
+ EXPECT_EQ(height, correctHeight);
+
+ [controller setMessageFrames:height];
+ NSRect msg3Frame = [controller getExtensionInstalledInfoMsgFrame];
+ // Bottom message should be kOuterVerticalMargin pixels above window edge.
+ EXPECT_EQ(msg3Frame.origin.y,
+ extension_installed_bubble::kOuterVerticalMargin);
+ NSRect msg2Frame = [controller getExtraInfoMsgFrame];
+ // Pageaction message should be kInnerVerticalMargin pixels above bottom msg.
+ EXPECT_EQ(msg2Frame.origin.y,
+ msg3Frame.origin.y + msg3Frame.size.height +
+ extension_installed_bubble::kInnerVerticalMargin);
+ NSRect msg1Frame = [controller getExtensionInstalledMsgFrame];
+ // Top message should be kInnerVerticalMargin pixels above Pageaction msg.
+ EXPECT_EQ(msg1Frame.origin.y,
+ msg2Frame.origin.y + msg2Frame.size.height +
+ extension_installed_bubble::kInnerVerticalMargin);
+
+ [controller setPageActionRemoved:YES];
+ [controller close];
+}
+
+TEST_F(ExtensionInstalledBubbleControllerTest, BrowserActionTest) {
+ extension_ = CreateExtension(extension_installed_bubble::kBrowserAction);
+ ExtensionInstalledBubbleControllerForTest* controller =
+ [[ExtensionInstalledBubbleControllerForTest alloc]
+ initWithParentWindow:window_
+ extension:extension_.get()
+ browser:browser_
+ icon:icon_];
+ EXPECT_TRUE(controller);
+
+ // Initialize window without having to calculate tabstrip locations.
+ [controller initializeWindow];
+ EXPECT_TRUE([controller window]);
+
+ int height = [controller calculateWindowHeight];
+ // Height should equal the vertical padding + height of all messages.
+ int correctHeight = 2 * extension_installed_bubble::kOuterVerticalMargin +
+ extension_installed_bubble::kInnerVerticalMargin +
+ [controller getExtensionInstalledMsgFrame].size.height +
+ [controller getExtensionInstalledInfoMsgFrame].size.height;
+ EXPECT_EQ(height, correctHeight);
+
+ [controller setMessageFrames:height];
+ NSRect msg3Frame = [controller getExtensionInstalledInfoMsgFrame];
+ // Bottom message should start kOuterVerticalMargin pixels above window edge.
+ EXPECT_EQ(msg3Frame.origin.y,
+ extension_installed_bubble::kOuterVerticalMargin);
+ NSRect msg1Frame = [controller getExtensionInstalledMsgFrame];
+ // Top message should start kInnerVerticalMargin pixels above top of
+ // extensionInstalled message, because page action message is hidden.
+ EXPECT_EQ(msg1Frame.origin.y,
+ msg3Frame.origin.y + msg3Frame.size.height +
+ extension_installed_bubble::kInnerVerticalMargin);
+
+ [controller close];
+}
diff --git a/chrome/browser/ui/cocoa/extension_view_mac.h b/chrome/browser/ui/cocoa/extension_view_mac.h
new file mode 100644
index 0000000..43e9058
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extension_view_mac.h
@@ -0,0 +1,88 @@
+// 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_UI_COCOA_EXTENSION_VIEW_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_EXTENSION_VIEW_MAC_H_
+#pragma once
+
+#include "base/basictypes.h"
+#include "gfx/native_widget_types.h"
+#include "gfx/size.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+class Browser;
+class ExtensionHost;
+class RenderViewHost;
+class RenderWidgetHostViewMac;
+class SkBitmap;
+
+// This class represents extension views. An extension view internally contains
+// a bridge to an extension process, which draws to the extension view's
+// native view object through IPC.
+class ExtensionViewMac {
+ public:
+ ExtensionViewMac(ExtensionHost* extension_host, Browser* browser);
+ ~ExtensionViewMac();
+
+ // Starts the extension process and creates the native view. You must call
+ // this method before calling any of this class's other methods.
+ void Init();
+
+ // Returns the extension's native view.
+ gfx::NativeView native_view();
+
+ // Returns the browser the extension belongs to.
+ Browser* browser() const { return browser_; }
+
+ // Does this extension live as a toolstrip in an extension shelf?
+ bool is_toolstrip() const { return is_toolstrip_; }
+ void set_is_toolstrip(bool is_toolstrip) { is_toolstrip_ = is_toolstrip; }
+
+ // Sets the extensions's background image.
+ void SetBackground(const SkBitmap& background);
+
+ // Method for the ExtensionHost to notify us about the correct size for
+ // extension contents.
+ void UpdatePreferredSize(const gfx::Size& new_size);
+
+ // Method for the ExtensionHost to notify us when the RenderViewHost has a
+ // connection.
+ void RenderViewCreated();
+
+ // Informs the view that its containing window's frame changed.
+ void WindowFrameChanged();
+
+ // The minimum/maximum dimensions of the popup.
+ // The minimum is just a little larger than the size of the button itself.
+ // The maximum is an arbitrary number that should be smaller than most
+ // screens.
+ static const CGFloat kMinWidth;
+ static const CGFloat kMinHeight;
+ static const CGFloat kMaxWidth;
+ static const CGFloat kMaxHeight;
+
+ private:
+ RenderViewHost* render_view_host() const;
+
+ void CreateWidgetHostView();
+
+ // True if the contents are being displayed inside the extension shelf.
+ bool is_toolstrip_;
+
+ Browser* browser_; // weak
+
+ ExtensionHost* extension_host_; // weak
+
+ // Created by us, but owned by its |native_view()|. We |release| the
+ // rwhv's native view in our destructor, effectively freeing this.
+ RenderWidgetHostViewMac* render_widget_host_view_;
+
+ // The background the view should have once it is initialized. This is set
+ // when the view has a custom background, but hasn't been initialized yet.
+ SkBitmap pending_background_;
+
+ DISALLOW_COPY_AND_ASSIGN(ExtensionViewMac);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_EXTENSION_VIEW_MAC_H_
diff --git a/chrome/browser/ui/cocoa/extension_view_mac.mm b/chrome/browser/ui/cocoa/extension_view_mac.mm
new file mode 100644
index 0000000..d3a10e7
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extension_view_mac.mm
@@ -0,0 +1,112 @@
+// 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 "chrome/browser/ui/cocoa/extension_view_mac.h"
+
+#include "chrome/browser/extensions/extension_host.h"
+#include "chrome/browser/renderer_host/render_view_host.h"
+#include "chrome/browser/renderer_host/render_widget_host_view_mac.h"
+
+// The minimum/maximum dimensions of the popup.
+const CGFloat ExtensionViewMac::kMinWidth = 25.0;
+const CGFloat ExtensionViewMac::kMinHeight = 25.0;
+const CGFloat ExtensionViewMac::kMaxWidth = 800.0;
+const CGFloat ExtensionViewMac::kMaxHeight = 600.0;
+
+ExtensionViewMac::ExtensionViewMac(ExtensionHost* extension_host,
+ Browser* browser)
+ : is_toolstrip_(true),
+ browser_(browser),
+ extension_host_(extension_host),
+ render_widget_host_view_(NULL) {
+ DCHECK(extension_host_);
+}
+
+ExtensionViewMac::~ExtensionViewMac() {
+ if (render_widget_host_view_)
+ [render_widget_host_view_->native_view() release];
+}
+
+void ExtensionViewMac::Init() {
+ CreateWidgetHostView();
+}
+
+gfx::NativeView ExtensionViewMac::native_view() {
+ DCHECK(render_widget_host_view_);
+ return render_widget_host_view_->native_view();
+}
+
+RenderViewHost* ExtensionViewMac::render_view_host() const {
+ return extension_host_->render_view_host();
+}
+
+void ExtensionViewMac::SetBackground(const SkBitmap& background) {
+ DCHECK(render_widget_host_view_);
+ if (render_view_host()->IsRenderViewLive()) {
+ render_widget_host_view_->SetBackground(background);
+ } else {
+ pending_background_ = background;
+ }
+}
+
+void ExtensionViewMac::UpdatePreferredSize(const gfx::Size& new_size) {
+ // TODO(thakis, erikkay): Windows does some tricks to resize the extension
+ // view not before it's visible. Do something similar here.
+
+ // No need to use CA here, our caller calls us repeatedly to animate the
+ // resizing.
+ NSView* view = native_view();
+ NSRect frame = [view frame];
+ frame.size.width = new_size.width();
+ frame.size.height = new_size.height();
+
+ // On first display of some extensions, this function is called with zero
+ // width after the correct size has been set. Bail if zero is seen, assuming
+ // that an extension's view doesn't want any dimensions to ever be zero.
+ // TODO(andybons): Verify this assumption and look into WebCore's
+ // |contentesPreferredWidth| to see why this is occurring.
+ if (NSIsEmptyRect(frame))
+ return;
+
+ DCHECK([view isKindOfClass:[RenderWidgetHostViewCocoa class]]);
+ RenderWidgetHostViewCocoa* hostView = (RenderWidgetHostViewCocoa*)view;
+
+ // RenderWidgetHostViewCocoa overrides setFrame but not setFrameSize.
+ // We need to defer the update back to the RenderWidgetHost so we don't
+ // get the flickering effect on 10.5 of http://crbug.com/31970
+ [hostView setFrameWithDeferredUpdate:frame];
+ [hostView setNeedsDisplay:YES];
+}
+
+void ExtensionViewMac::RenderViewCreated() {
+ // Do not allow webkit to draw scroll bars on views smaller than
+ // the largest size view allowed. The view will be resized to make
+ // scroll bars unnecessary. Scroll bars change the height of the
+ // view, so not drawing them is necessary to avoid infinite resizing.
+ gfx::Size largest_popup_size(
+ CGSizeMake(ExtensionViewMac::kMaxWidth, ExtensionViewMac::kMaxHeight));
+ extension_host_->DisableScrollbarsForSmallWindows(largest_popup_size);
+
+ if (!pending_background_.empty() && render_view_host()->view()) {
+ render_widget_host_view_->SetBackground(pending_background_);
+ pending_background_.reset();
+ }
+}
+
+void ExtensionViewMac::WindowFrameChanged() {
+ if (render_widget_host_view_)
+ render_widget_host_view_->WindowFrameChanged();
+}
+
+void ExtensionViewMac::CreateWidgetHostView() {
+ DCHECK(!render_widget_host_view_);
+ render_widget_host_view_ = new RenderWidgetHostViewMac(render_view_host());
+
+ // The RenderWidgetHostViewMac is owned by its native view, which is created
+ // in an autoreleased state. retain it, so that it doesn't immediately
+ // disappear.
+ [render_widget_host_view_->native_view() retain];
+
+ extension_host_->CreateRenderViewSoon(render_widget_host_view_);
+}
diff --git a/chrome/browser/ui/cocoa/extensions/browser_action_button.h b/chrome/browser/ui/cocoa/extensions/browser_action_button.h
new file mode 100644
index 0000000..a09ce7f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/browser_action_button.h
@@ -0,0 +1,98 @@
+// 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_UI_COCOA_EXTENSIONS_BROWSER_ACTION_BUTTON_H_
+#define CHROME_BROWSER_UI_COCOA_EXTENSIONS_BROWSER_ACTION_BUTTON_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#import "chrome/browser/ui/cocoa/gradient_button_cell.h"
+
+class Extension;
+class ExtensionAction;
+class ExtensionImageTrackerBridge;
+class Profile;
+
+// Fired when the Browser Action's state has changed. Usually the image needs to
+// be updated.
+extern NSString* const kBrowserActionButtonUpdatedNotification;
+
+// Fired on each drag event while the user is moving the button.
+extern NSString* const kBrowserActionButtonDraggingNotification;
+// Fired when the user drops the button.
+extern NSString* const kBrowserActionButtonDragEndNotification;
+
+@interface BrowserActionButton : NSButton {
+ @private
+ // Bridge to proxy Chrome notifications to the Obj-C class as well as load the
+ // extension's icon.
+ scoped_ptr<ExtensionImageTrackerBridge> imageLoadingBridge_;
+
+ // The default icon of the Button.
+ scoped_nsobject<NSImage> defaultIcon_;
+
+ // The icon specific to the active tab.
+ scoped_nsobject<NSImage> tabSpecificIcon_;
+
+ // Used to move the button and query whether a button is currently animating.
+ scoped_nsobject<NSViewAnimation> moveAnimation_;
+
+ // The extension for this button. Weak.
+ const Extension* extension_;
+
+ // The ID of the active tab.
+ int tabId_;
+
+ // Whether the button is currently being dragged.
+ BOOL isBeingDragged_;
+
+ // Drag events could be intercepted by other buttons, so to make sure that
+ // this is the only button moving if it ends up being dragged. This is set to
+ // YES upon |mouseDown:|.
+ BOOL dragCouldStart_;
+}
+
+- (id)initWithFrame:(NSRect)frame
+ extension:(const Extension*)extension
+ profile:(Profile*)profile
+ tabId:(int)tabId;
+
+- (void)setFrame:(NSRect)frameRect animate:(BOOL)animate;
+
+- (void)setDefaultIcon:(NSImage*)image;
+
+- (void)setTabSpecificIcon:(NSImage*)image;
+
+- (void)updateState;
+
+- (BOOL)isAnimating;
+
+// Returns a pointer to an autoreleased NSImage with the badge, shadow and
+// cell image drawn into it.
+- (NSImage*)compositedImage;
+
+@property(readonly, nonatomic) BOOL isBeingDragged;
+@property(readonly, nonatomic) const Extension* extension;
+@property(readwrite, nonatomic) int tabId;
+
+@end
+
+@interface BrowserActionCell : GradientButtonCell {
+ @private
+ // The current tab ID used when drawing the cell.
+ int tabId_;
+
+ // The action we're drawing the cell for. Weak.
+ ExtensionAction* extensionAction_;
+}
+
+@property(readwrite, nonatomic) int tabId;
+@property(readwrite, nonatomic) ExtensionAction* extensionAction;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_EXTENSIONS_BROWSER_ACTION_BUTTON_H_
diff --git a/chrome/browser/ui/cocoa/extensions/browser_action_button.mm b/chrome/browser/ui/cocoa/extensions/browser_action_button.mm
new file mode 100644
index 0000000..14701c8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/browser_action_button.mm
@@ -0,0 +1,333 @@
+// 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/ui/cocoa/extensions/browser_action_button.h"
+
+#include <algorithm>
+#include <cmath>
+
+#include "base/logging.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/extensions/image_loading_tracker.h"
+#include "chrome/browser/ui/cocoa/extensions/extension_action_context_menu.h"
+#import "chrome/browser/ui/cocoa/image_utils.h"
+#include "chrome/common/extensions/extension.h"
+#include "chrome/common/extensions/extension_action.h"
+#include "chrome/common/extensions/extension_resource.h"
+#include "chrome/common/notification_observer.h"
+#include "chrome/common/notification_registrar.h"
+#include "chrome/common/notification_source.h"
+#include "chrome/common/notification_type.h"
+#include "gfx/canvas_skia_paint.h"
+#include "gfx/rect.h"
+#include "gfx/size.h"
+#include "skia/ext/skia_utils_mac.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+
+NSString* const kBrowserActionButtonUpdatedNotification =
+ @"BrowserActionButtonUpdatedNotification";
+
+NSString* const kBrowserActionButtonDraggingNotification =
+ @"BrowserActionButtonDraggingNotification";
+NSString* const kBrowserActionButtonDragEndNotification =
+ @"BrowserActionButtonDragEndNotification";
+
+static const CGFloat kBrowserActionBadgeOriginYOffset = 5;
+
+namespace {
+const CGFloat kAnimationDuration = 0.2;
+const CGFloat kShadowOffset = 2.0;
+} // anonymous namespace
+
+// A helper class to bridge the asynchronous Skia bitmap loading mechanism to
+// the extension's button.
+class ExtensionImageTrackerBridge : public NotificationObserver,
+ public ImageLoadingTracker::Observer {
+ public:
+ ExtensionImageTrackerBridge(BrowserActionButton* owner,
+ const Extension* extension)
+ : owner_(owner),
+ tracker_(this) {
+ // The Browser Action API does not allow the default icon path to be
+ // changed at runtime, so we can load this now and cache it.
+ std::string path = extension->browser_action()->default_icon_path();
+ if (!path.empty()) {
+ tracker_.LoadImage(extension, extension->GetResource(path),
+ gfx::Size(Extension::kBrowserActionIconMaxSize,
+ Extension::kBrowserActionIconMaxSize),
+ ImageLoadingTracker::DONT_CACHE);
+ }
+ registrar_.Add(this, NotificationType::EXTENSION_BROWSER_ACTION_UPDATED,
+ Source<ExtensionAction>(extension->browser_action()));
+ }
+
+ ~ExtensionImageTrackerBridge() {}
+
+ // ImageLoadingTracker::Observer implementation.
+ void OnImageLoaded(SkBitmap* image, ExtensionResource resource, int index) {
+ if (image)
+ [owner_ setDefaultIcon:gfx::SkBitmapToNSImage(*image)];
+ [owner_ updateState];
+ }
+
+ // Overridden from NotificationObserver.
+ void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ if (type == NotificationType::EXTENSION_BROWSER_ACTION_UPDATED)
+ [owner_ updateState];
+ else
+ NOTREACHED();
+ }
+
+ private:
+ // Weak. Owns us.
+ BrowserActionButton* owner_;
+
+ // Loads the button's icons for us on the file thread.
+ ImageLoadingTracker tracker_;
+
+ // Used for registering to receive notifications and automatic clean up.
+ NotificationRegistrar registrar_;
+
+ DISALLOW_COPY_AND_ASSIGN(ExtensionImageTrackerBridge);
+};
+
+@interface BrowserActionCell(Internals)
+- (void)setIconShadow;
+- (void)drawBadgeWithinFrame:(NSRect)frame;
+@end
+
+@interface BrowserActionButton(Private)
+- (void)endDrag;
+@end
+
+@implementation BrowserActionButton
+
+@synthesize isBeingDragged = isBeingDragged_;
+@synthesize extension = extension_;
+@synthesize tabId = tabId_;
+
++ (Class)cellClass {
+ return [BrowserActionCell class];
+}
+
+- (id)initWithFrame:(NSRect)frame
+ extension:(const Extension*)extension
+ profile:(Profile*)profile
+ tabId:(int)tabId {
+ if ((self = [super initWithFrame:frame])) {
+ BrowserActionCell* cell = [[[BrowserActionCell alloc] init] autorelease];
+ // [NSButton setCell:] warns to NOT use setCell: other than in the
+ // initializer of a control. However, we are using a basic
+ // NSButton whose initializer does not take an NSCell as an
+ // object. To honor the assumed semantics, we do nothing with
+ // NSButton between alloc/init and setCell:.
+ [self setCell:cell];
+ [cell setTabId:tabId];
+ [cell setExtensionAction:extension->browser_action()];
+
+ [self setTitle:@""];
+ [self setButtonType:NSMomentaryChangeButton];
+ [self setShowsBorderOnlyWhileMouseInside:YES];
+
+ [self setMenu:[[[ExtensionActionContextMenu alloc]
+ initWithExtension:extension
+ profile:profile
+ extensionAction:extension->browser_action()] autorelease]];
+
+ tabId_ = tabId;
+ extension_ = extension;
+ imageLoadingBridge_.reset(new ExtensionImageTrackerBridge(self, extension));
+
+ moveAnimation_.reset([[NSViewAnimation alloc] init]);
+ [moveAnimation_ gtm_setDuration:kAnimationDuration
+ eventMask:NSLeftMouseUpMask];
+ [moveAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
+
+ [self updateState];
+ }
+
+ return self;
+}
+
+- (BOOL)acceptsFirstResponder {
+ return YES;
+}
+
+- (void)mouseDown:(NSEvent*)theEvent {
+ [[self cell] setHighlighted:YES];
+ dragCouldStart_ = YES;
+}
+
+- (void)mouseDragged:(NSEvent*)theEvent {
+ if (!dragCouldStart_)
+ return;
+
+ if (!isBeingDragged_) {
+ // The start of a drag. Position the button above all others.
+ [[self superview] addSubview:self positioned:NSWindowAbove relativeTo:nil];
+ }
+ isBeingDragged_ = YES;
+ NSRect buttonFrame = [self frame];
+ // TODO(andybons): Constrain the buttons to be within the container.
+ // Clamp the button to be within its superview along the X-axis.
+ buttonFrame.origin.x += [theEvent deltaX];
+ [self setFrame:buttonFrame];
+ [self setNeedsDisplay:YES];
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kBrowserActionButtonDraggingNotification
+ object:self];
+}
+
+- (void)mouseUp:(NSEvent*)theEvent {
+ dragCouldStart_ = NO;
+ // There are non-drag cases where a mouseUp: may happen
+ // (e.g. mouse-down, cmd-tab to another application, move mouse,
+ // mouse-up).
+ NSPoint location = [self convertPoint:[theEvent locationInWindow]
+ fromView:nil];
+ if (NSPointInRect(location, [self bounds]) && !isBeingDragged_) {
+ // Only perform the click if we didn't drag the button.
+ [self performClick:self];
+ } else {
+ // Make sure an ESC to end a drag doesn't trigger 2 endDrags.
+ if (isBeingDragged_) {
+ [self endDrag];
+ } else {
+ [super mouseUp:theEvent];
+ }
+ }
+}
+
+- (void)endDrag {
+ isBeingDragged_ = NO;
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kBrowserActionButtonDragEndNotification
+ object:self];
+ [[self cell] setHighlighted:NO];
+}
+
+- (void)setFrame:(NSRect)frameRect animate:(BOOL)animate {
+ if (!animate) {
+ [self setFrame:frameRect];
+ } else {
+ if ([moveAnimation_ isAnimating])
+ [moveAnimation_ stopAnimation];
+
+ NSDictionary* animationDictionary =
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ self, NSViewAnimationTargetKey,
+ [NSValue valueWithRect:[self frame]], NSViewAnimationStartFrameKey,
+ [NSValue valueWithRect:frameRect], NSViewAnimationEndFrameKey,
+ nil];
+ [moveAnimation_ setViewAnimations:
+ [NSArray arrayWithObject:animationDictionary]];
+ [moveAnimation_ startAnimation];
+ }
+}
+
+- (void)setDefaultIcon:(NSImage*)image {
+ defaultIcon_.reset([image retain]);
+}
+
+- (void)setTabSpecificIcon:(NSImage*)image {
+ tabSpecificIcon_.reset([image retain]);
+}
+
+- (void)updateState {
+ if (tabId_ < 0)
+ return;
+
+ std::string tooltip = extension_->browser_action()->GetTitle(tabId_);
+ if (tooltip.empty()) {
+ [self setToolTip:nil];
+ } else {
+ [self setToolTip:base::SysUTF8ToNSString(tooltip)];
+ }
+
+ SkBitmap image = extension_->browser_action()->GetIcon(tabId_);
+ if (!image.isNull()) {
+ [self setTabSpecificIcon:gfx::SkBitmapToNSImage(image)];
+ [self setImage:tabSpecificIcon_];
+ } else if (defaultIcon_) {
+ [self setImage:defaultIcon_];
+ }
+
+ [[self cell] setTabId:tabId_];
+
+ [self setNeedsDisplay:YES];
+
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kBrowserActionButtonUpdatedNotification
+ object:self];
+}
+
+- (BOOL)isAnimating {
+ return [moveAnimation_ isAnimating];
+}
+
+- (NSImage*)compositedImage {
+ NSRect bounds = [self bounds];
+ NSImage* image = [[[NSImage alloc] initWithSize:bounds.size] autorelease];
+ [image lockFocus];
+
+ [[NSColor clearColor] set];
+ NSRectFill(bounds);
+ [[self cell] setIconShadow];
+
+ NSImage* actionImage = [self image];
+ const NSSize imageSize = [actionImage size];
+ const NSRect imageRect =
+ NSMakeRect(std::floor((NSWidth(bounds) - imageSize.width) / 2.0),
+ std::floor((NSHeight(bounds) - imageSize.height) / 2.0),
+ imageSize.width, imageSize.height);
+ [actionImage drawInRect:imageRect
+ fromRect:NSZeroRect
+ operation:NSCompositeSourceOver
+ fraction:1.0
+ neverFlipped:YES];
+
+ bounds.origin.y += kShadowOffset - kBrowserActionBadgeOriginYOffset;
+ bounds.origin.x -= kShadowOffset;
+ [[self cell] drawBadgeWithinFrame:bounds];
+
+ [image unlockFocus];
+ return image;
+}
+
+@end
+
+@implementation BrowserActionCell
+
+@synthesize tabId = tabId_;
+@synthesize extensionAction = extensionAction_;
+
+- (void)setIconShadow {
+ // Create the shadow below and to the right of the drawn image.
+ scoped_nsobject<NSShadow> imgShadow([[NSShadow alloc] init]);
+ [imgShadow.get() setShadowOffset:NSMakeSize(kShadowOffset, -kShadowOffset)];
+ [imgShadow setShadowBlurRadius:2.0];
+ [imgShadow.get() setShadowColor:[[NSColor blackColor]
+ colorWithAlphaComponent:0.3]];
+ [imgShadow set];
+}
+
+- (void)drawBadgeWithinFrame:(NSRect)frame {
+ gfx::CanvasSkiaPaint canvas(frame, false);
+ canvas.set_composite_alpha(true);
+ gfx::Rect boundingRect(NSRectToCGRect(frame));
+ extensionAction_->PaintBadge(&canvas, boundingRect, tabId_);
+}
+
+- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
+ [NSGraphicsContext saveGraphicsState];
+ [self setIconShadow];
+ [super drawInteriorWithFrame:cellFrame inView:controlView];
+ cellFrame.origin.y += kBrowserActionBadgeOriginYOffset;
+ [self drawBadgeWithinFrame:cellFrame];
+ [NSGraphicsContext restoreGraphicsState];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h b/chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h
new file mode 100644
index 0000000..d9ebf6e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h
@@ -0,0 +1,84 @@
+// 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_UI_COCOA_EXTENSIONS_BROWSER_ACTIONS_CONTAINER_VIEW_
+#define CHROME_BROWSER_UI_COCOA_EXTENSIONS_BROWSER_ACTIONS_CONTAINER_VIEW_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// Sent when a user-initiated drag to resize the container is initiated.
+extern NSString* const kBrowserActionGrippyDragStartedNotification;
+
+// Sent when a user-initiated drag is resizing the container.
+extern NSString* const kBrowserActionGrippyDraggingNotification;
+
+// Sent when a user-initiated drag to resize the container has finished.
+extern NSString* const kBrowserActionGrippyDragFinishedNotification;
+
+// The view that encompasses the Browser Action buttons in the toolbar and
+// provides mechanisms for resizing.
+@interface BrowserActionsContainerView : NSView {
+ @private
+ // The frame encompasing the grippy used for resizing the container.
+ NSRect grippyRect_;
+
+ // The end frame of the animation currently running for this container or
+ // NSZeroRect if none is in progress.
+ NSRect animationEndFrame_;
+
+ // Used to cache the original position within the container that initiated the
+ // drag.
+ NSPoint initialDragPoint_;
+
+ // Used to cache the previous x-pos of the frame rect for resizing purposes.
+ CGFloat lastXPos_;
+
+ // The maximum width of the container.
+ CGFloat maxWidth_;
+
+ // Whether the container is currently being resized by the user.
+ BOOL userIsResizing_;
+
+ // Whether the user can resize this at all. Resizing is disabled in incognito
+ // mode since any changes done in incognito mode are not saved anyway, and
+ // also to avoid a crash. http://crbug.com/42848
+ BOOL resizable_;
+
+ // Whether the user is allowed to drag the grippy to the left. NO if all
+ // extensions are shown or the location bar has hit its minimum width (handled
+ // within toolbar_controller.mm).
+ BOOL canDragLeft_;
+
+ // Whether the user is allowed to drag the grippy to the right. NO if all
+ // extensions are hidden.
+ BOOL canDragRight_;
+
+ // When the left grippy is pinned, resizing the window has no effect on its
+ // position. This prevents it from overlapping with other elements as well
+ // as letting the container expand when the window is going from super small
+ // to large.
+ BOOL grippyPinned_;
+}
+
+// Resizes the container to the given ideal width, adjusting the |lastXPos_| so
+// that |resizeDeltaX| is accurate.
+- (void)resizeToWidth:(CGFloat)width animate:(BOOL)animate;
+
+// Returns the change in the x-pos of the frame rect during resizing. Meant to
+// be queried when a NSViewFrameDidChangeNotification is fired to determine
+// placement of surrounding elements.
+- (CGFloat)resizeDeltaX;
+
+@property(nonatomic, readonly) NSRect animationEndFrame;
+@property(nonatomic) BOOL canDragLeft;
+@property(nonatomic) BOOL canDragRight;
+@property(nonatomic) BOOL grippyPinned;
+@property(nonatomic,getter=isResizable) BOOL resizable;
+@property(nonatomic) CGFloat maxWidth;
+@property(readonly, nonatomic) BOOL userIsResizing;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_EXTENSIONS_BROWSER_ACTIONS_CONTAINER_VIEW_
diff --git a/chrome/browser/ui/cocoa/extensions/browser_actions_container_view.mm b/chrome/browser/ui/cocoa/extensions/browser_actions_container_view.mm
new file mode 100644
index 0000000..3d10e22
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/browser_actions_container_view.mm
@@ -0,0 +1,192 @@
+// 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/ui/cocoa/extensions/browser_actions_container_view.h"
+
+#include <algorithm>
+
+#include "base/basictypes.h"
+#import "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+
+NSString* const kBrowserActionGrippyDragStartedNotification =
+ @"BrowserActionGrippyDragStartedNotification";
+NSString* const kBrowserActionGrippyDraggingNotification =
+ @"BrowserActionGrippyDraggingNotification";
+NSString* const kBrowserActionGrippyDragFinishedNotification =
+ @"BrowserActionGrippyDragFinishedNotification";
+
+namespace {
+const CGFloat kAnimationDuration = 0.2;
+const CGFloat kGrippyWidth = 4.0;
+const CGFloat kMinimumContainerWidth = 10.0;
+} // namespace
+
+@interface BrowserActionsContainerView(Private)
+// Returns the cursor that should be shown when hovering over the grippy based
+// on |canDragLeft_| and |canDragRight_|.
+- (NSCursor*)appropriateCursorForGrippy;
+@end
+
+@implementation BrowserActionsContainerView
+
+@synthesize animationEndFrame = animationEndFrame_;
+@synthesize canDragLeft = canDragLeft_;
+@synthesize canDragRight = canDragRight_;
+@synthesize grippyPinned = grippyPinned_;
+@synthesize maxWidth = maxWidth_;
+@synthesize userIsResizing = userIsResizing_;
+
+#pragma mark -
+#pragma mark Overridden Class Functions
+
+- (id)initWithFrame:(NSRect)frameRect {
+ if ((self = [super initWithFrame:frameRect])) {
+ grippyRect_ = NSMakeRect(0.0, 0.0, kGrippyWidth, NSHeight([self bounds]));
+ canDragLeft_ = YES;
+ canDragRight_ = YES;
+ resizable_ = YES;
+ [self setHidden:YES];
+ }
+ return self;
+}
+
+- (void)setResizable:(BOOL)resizable {
+ if (resizable == resizable_)
+ return;
+ resizable_ = resizable;
+ [self setNeedsDisplay:YES];
+}
+
+- (BOOL)isResizable {
+ return resizable_;
+}
+
+- (void)resetCursorRects {
+ [self discardCursorRects];
+ [self addCursorRect:grippyRect_ cursor:[self appropriateCursorForGrippy]];
+}
+
+- (BOOL)acceptsFirstResponder {
+ return YES;
+}
+
+- (void)mouseDown:(NSEvent*)theEvent {
+ initialDragPoint_ = [self convertPoint:[theEvent locationInWindow]
+ fromView:nil];
+ if (!resizable_ ||
+ !NSMouseInRect(initialDragPoint_, grippyRect_, [self isFlipped]))
+ return;
+
+ lastXPos_ = [self frame].origin.x;
+ userIsResizing_ = YES;
+
+ [[self appropriateCursorForGrippy] push];
+ // Disable cursor rects so that the Omnibox and other UI elements don't push
+ // cursors while the user is dragging. The cursor should be grippy until
+ // the |-mouseUp:| message is received.
+ [[self window] disableCursorRects];
+
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kBrowserActionGrippyDragStartedNotification
+ object:self];
+}
+
+- (void)mouseUp:(NSEvent*)theEvent {
+ if (!userIsResizing_)
+ return;
+
+ [NSCursor pop];
+ [[self window] enableCursorRects];
+
+ userIsResizing_ = NO;
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kBrowserActionGrippyDragFinishedNotification
+ object:self];
+}
+
+- (void)mouseDragged:(NSEvent*)theEvent {
+ if (!userIsResizing_)
+ return;
+
+ NSPoint location = [self convertPoint:[theEvent locationInWindow]
+ fromView:nil];
+ NSRect containerFrame = [self frame];
+ CGFloat dX = [theEvent deltaX];
+ CGFloat withDelta = location.x - dX;
+ canDragRight_ = (withDelta >= initialDragPoint_.x) &&
+ (NSWidth(containerFrame) > kMinimumContainerWidth);
+ canDragLeft_ = (withDelta <= initialDragPoint_.x) &&
+ (NSWidth(containerFrame) < maxWidth_);
+ if ((dX < 0.0 && !canDragLeft_) || (dX > 0.0 && !canDragRight_))
+ return;
+
+ containerFrame.size.width =
+ std::max(NSWidth(containerFrame) - dX, kMinimumContainerWidth);
+
+ if (NSWidth(containerFrame) == kMinimumContainerWidth)
+ return;
+
+ containerFrame.origin.x += dX;
+
+ [self setFrame:containerFrame];
+ [self setNeedsDisplay:YES];
+
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kBrowserActionGrippyDraggingNotification
+ object:self];
+
+ lastXPos_ += dX;
+}
+
+- (ViewID)viewID {
+ return VIEW_ID_BROWSER_ACTION_TOOLBAR;
+}
+
+#pragma mark -
+#pragma mark Public Methods
+
+- (void)resizeToWidth:(CGFloat)width animate:(BOOL)animate {
+ width = std::max(width, kMinimumContainerWidth);
+ NSRect frame = [self frame];
+ lastXPos_ = frame.origin.x;
+ CGFloat dX = frame.size.width - width;
+ frame.size.width = width;
+ NSRect newFrame = NSOffsetRect(frame, dX, 0);
+ if (animate) {
+ [NSAnimationContext beginGrouping];
+ [[NSAnimationContext currentContext] setDuration:kAnimationDuration];
+ [[self animator] setFrame:newFrame];
+ [NSAnimationContext endGrouping];
+ animationEndFrame_ = newFrame;
+ } else {
+ [self setFrame:newFrame];
+ [self setNeedsDisplay:YES];
+ }
+}
+
+- (CGFloat)resizeDeltaX {
+ return [self frame].origin.x - lastXPos_;
+}
+
+#pragma mark -
+#pragma mark Private Methods
+
+// Returns the cursor to display over the grippy hover region depending on the
+// current drag state.
+- (NSCursor*)appropriateCursorForGrippy {
+ NSCursor* retVal;
+ if (!resizable_ || (!canDragLeft_ && !canDragRight_)) {
+ retVal = [NSCursor arrowCursor];
+ } else if (!canDragLeft_) {
+ retVal = [NSCursor resizeRightCursor];
+ } else if (!canDragRight_) {
+ retVal = [NSCursor resizeLeftCursor];
+ } else {
+ retVal = [NSCursor resizeLeftRightCursor];
+ }
+ return retVal;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/extensions/browser_actions_container_view_unittest.mm b/chrome/browser/ui/cocoa/extensions/browser_actions_container_view_unittest.mm
new file mode 100644
index 0000000..002aa05
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/browser_actions_container_view_unittest.mm
@@ -0,0 +1,52 @@
+// 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/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+const CGFloat kContainerHeight = 15.0;
+const CGFloat kMinimumContainerWidth = 10.0;
+
+class BrowserActionsContainerViewTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ view_.reset([[BrowserActionsContainerView alloc]
+ initWithFrame:NSMakeRect(0, 0, 0, kContainerHeight)]);
+ }
+
+ scoped_nsobject<BrowserActionsContainerView> view_;
+};
+
+TEST_F(BrowserActionsContainerViewTest, BasicTests) {
+ EXPECT_TRUE([view_ isResizable]);
+ EXPECT_TRUE([view_ canDragLeft]);
+ EXPECT_TRUE([view_ canDragRight]);
+ EXPECT_TRUE([view_ isHidden]);
+}
+
+TEST_F(BrowserActionsContainerViewTest, SetWidthTests) {
+ // Try setting below the minimum width (10 pixels).
+ [view_ resizeToWidth:5.0 animate:NO];
+ EXPECT_EQ(kMinimumContainerWidth, NSWidth([view_ frame])) << "Frame width is "
+ << "less than the minimum allowed.";
+ // Since the frame expands to the left, the x-position delta value will be
+ // negative.
+ EXPECT_EQ(-kMinimumContainerWidth, [view_ resizeDeltaX]);
+
+ [view_ resizeToWidth:35.0 animate:NO];
+ EXPECT_EQ(35.0, NSWidth([view_ frame]));
+ EXPECT_EQ(-25.0, [view_ resizeDeltaX]);
+
+ [view_ resizeToWidth:20.0 animate:NO];
+ EXPECT_EQ(20.0, NSWidth([view_ frame]));
+ EXPECT_EQ(15.0, [view_ resizeDeltaX]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/extensions/browser_actions_controller.h b/chrome/browser/ui/cocoa/extensions/browser_actions_controller.h
new file mode 100644
index 0000000..1c8b700
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/browser_actions_controller.h
@@ -0,0 +1,117 @@
+// 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_UI_COCOA_EXTENSIONS_BROWSER_ACTIONS_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_EXTENSIONS_BROWSER_ACTIONS_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+
+class Browser;
+@class BrowserActionButton;
+@class BrowserActionsContainerView;
+class Extension;
+@class ExtensionPopupController;
+class ExtensionToolbarModel;
+class ExtensionsServiceObserverBridge;
+@class MenuButton;
+class PrefService;
+class Profile;
+
+// Sent when the visibility of the Browser Actions changes.
+extern NSString* const kBrowserActionVisibilityChangedNotification;
+
+// Handles state and provides an interface for controlling the Browser Actions
+// container within the Toolbar.
+@interface BrowserActionsController : NSObject {
+ @private
+ // Reference to the current browser. Weak.
+ Browser* browser_;
+
+ // The view from Toolbar.xib we'll be rendering our browser actions in. Weak.
+ BrowserActionsContainerView* containerView_;
+
+ // The current profile. Weak.
+ Profile* profile_;
+
+ // The model that tracks the order of the toolbar icons. Weak.
+ ExtensionToolbarModel* toolbarModel_;
+
+ // The observer for the ExtensionsService we're getting events from.
+ scoped_ptr<ExtensionsServiceObserverBridge> observer_;
+
+ // A dictionary of Extension ID -> BrowserActionButton pairs representing the
+ // buttons present in the container view. The ID is a string unique to each
+ // extension.
+ scoped_nsobject<NSMutableDictionary> buttons_;
+
+ // Array of hidden buttons in the correct order in which the user specified.
+ scoped_nsobject<NSMutableArray> hiddenButtons_;
+
+ // The currently running chevron animation (fade in/out).
+ scoped_nsobject<NSViewAnimation> chevronAnimation_;
+
+ // The chevron button used when Browser Actions are hidden.
+ scoped_nsobject<MenuButton> chevronMenuButton_;
+
+ // The Browser Actions overflow menu.
+ scoped_nsobject<NSMenu> overflowMenu_;
+}
+
+@property(readonly, nonatomic) BrowserActionsContainerView* containerView;
+
+// Initializes the controller given the current browser and container view that
+// will hold the browser action buttons.
+- (id)initWithBrowser:(Browser*)browser
+ containerView:(BrowserActionsContainerView*)container;
+
+// Update the display of all buttons.
+- (void)update;
+
+// Returns the current number of browser action buttons within the container,
+// whether or not they are displayed.
+- (NSUInteger)buttonCount;
+
+// Returns the current number of browser action buttons displayed in the
+// container.
+- (NSUInteger)visibleButtonCount;
+
+// Returns a pointer to the chevron menu button.
+- (MenuButton*)chevronMenuButton;
+
+// Resizes the container given the number of visible buttons, taking into
+// account the size of the grippy. Also updates the persistent width preference.
+- (void)resizeContainerAndAnimate:(BOOL)animate;
+
+// Returns the NSView for the action button associated with an extension.
+- (NSView*)browserActionViewForExtension:(const Extension*)extension;
+
+// Returns the saved width determined by the number of shown Browser Actions
+// preference property. If no preference is found, then the width for the
+// container is returned as if all buttons are shown.
+- (CGFloat)savedWidth;
+
+// Returns where the popup arrow should point to for a given Browser Action. If
+// it is passed an extension that is not a Browser Action, then it will return
+// NSZeroPoint.
+- (NSPoint)popupPointForBrowserAction:(const Extension*)extension;
+
+// Returns whether the chevron button is currently hidden or in the process of
+// being hidden (fading out). Will return NO if it is not hidden or is in the
+// process of fading in.
+- (BOOL)chevronIsHidden;
+
+// Registers the user preferences used by this class.
++ (void)registerUserPrefs:(PrefService*)prefs;
+
+@end // @interface BrowserActionsController
+
+@interface BrowserActionsController(TestingAPI)
+- (NSButton*)buttonWithIndex:(NSUInteger)index;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_EXTENSIONS_BROWSER_ACTIONS_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/extensions/browser_actions_controller.mm b/chrome/browser/ui/cocoa/extensions/browser_actions_controller.mm
new file mode 100644
index 0000000..2ae72a4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/browser_actions_controller.mm
@@ -0,0 +1,863 @@
+// 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 "browser_actions_controller.h"
+
+#include <cmath>
+#include <string>
+
+#include "base/nsimage_cache_mac.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/extensions/extension_browser_event_router.h"
+#include "chrome/browser/extensions/extension_host.h"
+#include "chrome/browser/extensions/extension_toolbar_model.h"
+#include "chrome/browser/extensions/extensions_service.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/ui/browser.h"
+#import "chrome/browser/ui/cocoa/extensions/browser_action_button.h"
+#import "chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h"
+#import "chrome/browser/ui/cocoa/extensions/chevron_menu_button.h"
+#import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
+#import "chrome/browser/ui/cocoa/menu_button.h"
+#include "chrome/common/extensions/extension_action.h"
+#include "chrome/common/notification_observer.h"
+#include "chrome/common/notification_registrar.h"
+#include "chrome/common/pref_names.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+
+NSString* const kBrowserActionVisibilityChangedNotification =
+ @"BrowserActionVisibilityChangedNotification";
+
+namespace {
+const CGFloat kAnimationDuration = 0.2;
+
+const CGFloat kChevronWidth = 14.0;
+
+// Image used for the overflow button.
+NSString* const kOverflowChevronsName =
+ @"browser_actions_overflow_Template.pdf";
+
+// Since the container is the maximum height of the toolbar, we have
+// to move the buttons up by this amount in order to have them look
+// vertically centered within the toolbar.
+const CGFloat kBrowserActionOriginYOffset = 5.0;
+
+// The size of each button on the toolbar.
+const CGFloat kBrowserActionHeight = 29.0;
+const CGFloat kBrowserActionWidth = 29.0;
+
+// The padding between browser action buttons.
+const CGFloat kBrowserActionButtonPadding = 2.0;
+
+// Padding between Omnibox and first button. Since the buttons have a
+// pixel of internal padding, this needs an extra pixel.
+const CGFloat kBrowserActionLeftPadding = kBrowserActionButtonPadding + 1.0;
+
+// How far to inset from the bottom of the view to get the top border
+// of the popup 2px below the bottom of the Omnibox.
+const CGFloat kBrowserActionBubbleYOffset = 3.0;
+
+} // namespace
+
+@interface BrowserActionsController(Private)
+// Used during initialization to create the BrowserActionButton objects from the
+// stored toolbar model.
+- (void)createButtons;
+
+// Creates and then adds the given extension's action button to the container
+// at the given index within the container. It does not affect the toolbar model
+// object since it is called when the toolbar model changes.
+- (void)createActionButtonForExtension:(const Extension*)extension
+ withIndex:(NSUInteger)index;
+
+// Removes an action button for the given extension from the container. This
+// method also does not affect the underlying toolbar model since it is called
+// when the toolbar model changes.
+- (void)removeActionButtonForExtension:(const Extension*)extension;
+
+// Useful in the case of a Browser Action being added/removed from the middle of
+// the container, this method repositions each button according to the current
+// toolbar model.
+- (void)positionActionButtonsAndAnimate:(BOOL)animate;
+
+// During container resizing, buttons become more transparent as they are pushed
+// off the screen. This method updates each button's opacity determined by the
+// position of the button.
+- (void)updateButtonOpacity;
+
+// Returns the existing button with the given extension backing it; nil if it
+// cannot be found or the extension's ID is invalid.
+- (BrowserActionButton*)buttonForExtension:(const Extension*)extension;
+
+// Returns the preferred width of the container given the number of visible
+// buttons |buttonCount|.
+- (CGFloat)containerWidthWithButtonCount:(NSUInteger)buttonCount;
+
+// Returns the number of buttons that can fit in the container according to its
+// current size.
+- (NSUInteger)containerButtonCapacity;
+
+// Notification handlers for events registered by the class.
+
+// Updates each button's opacity, the cursor rects and chevron position.
+- (void)containerFrameChanged:(NSNotification*)notification;
+
+// Hides the chevron and unhides every hidden button so that dragging the
+// container out smoothly shows the Browser Action buttons.
+- (void)containerDragStart:(NSNotification*)notification;
+
+// Sends a notification for the toolbar to reposition surrounding UI elements.
+- (void)containerDragging:(NSNotification*)notification;
+
+// Determines which buttons need to be hidden based on the new size, hides them
+// and updates the chevron overflow menu. Also fires a notification to let the
+// toolbar know that the drag has finished.
+- (void)containerDragFinished:(NSNotification*)notification;
+
+// Updates the image associated with the button should it be within the chevron
+// menu.
+- (void)actionButtonUpdated:(NSNotification*)notification;
+
+// Adjusts the position of the surrounding action buttons depending on where the
+// button is within the container.
+- (void)actionButtonDragging:(NSNotification*)notification;
+
+// Updates the position of the Browser Actions within the container. This fires
+// when _any_ Browser Action button is done dragging to keep all open windows in
+// sync visually.
+- (void)actionButtonDragFinished:(NSNotification*)notification;
+
+// Moves the given button both visually and within the toolbar model to the
+// specified index.
+- (void)moveButton:(BrowserActionButton*)button
+ toIndex:(NSUInteger)index
+ animate:(BOOL)animate;
+
+// Handles when the given BrowserActionButton object is clicked.
+- (void)browserActionClicked:(BrowserActionButton*)button;
+
+// Returns whether the given extension should be displayed. Only displays
+// incognito-enabled extensions in incognito mode. Otherwise returns YES.
+- (BOOL)shouldDisplayBrowserAction:(const Extension*)extension;
+
+// The reason |frame| is specified in these chevron functions is because the
+// container may be animating and the end frame of the animation should be
+// passed instead of the current frame (which may be off and cause the chevron
+// to jump at the end of its animation).
+
+// Shows the overflow chevron button depending on whether there are any hidden
+// extensions within the frame given.
+- (void)showChevronIfNecessaryInFrame:(NSRect)frame animate:(BOOL)animate;
+
+// Moves the chevron to its correct position within |frame|.
+- (void)updateChevronPositionInFrame:(NSRect)frame;
+
+// Shows or hides the chevron, animating as specified by |animate|.
+- (void)setChevronHidden:(BOOL)hidden
+ inFrame:(NSRect)frame
+ animate:(BOOL)animate;
+
+// Handles when a menu item within the chevron overflow menu is selected.
+- (void)chevronItemSelected:(id)menuItem;
+
+// Clears and then populates the overflow menu based on the contents of
+// |hiddenButtons_|.
+- (void)updateOverflowMenu;
+
+// Updates the container's grippy cursor based on the number of hidden buttons.
+- (void)updateGrippyCursors;
+
+// Returns the ID of the currently selected tab or -1 if none exists.
+- (int)currentTabId;
+@end
+
+// A helper class to proxy extension notifications to the view controller's
+// appropriate methods.
+class ExtensionsServiceObserverBridge : public NotificationObserver,
+ public ExtensionToolbarModel::Observer {
+ public:
+ ExtensionsServiceObserverBridge(BrowserActionsController* owner,
+ Profile* profile) : owner_(owner) {
+ registrar_.Add(this, NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE,
+ Source<Profile>(profile));
+ }
+
+ // Overridden from NotificationObserver.
+ void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ switch (type.value) {
+ case NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE: {
+ ExtensionPopupController* popup = [ExtensionPopupController popup];
+ if (popup && ![popup isClosing])
+ [popup close];
+
+ break;
+ }
+ default:
+ NOTREACHED() << L"Unexpected notification";
+ }
+ }
+
+ // ExtensionToolbarModel::Observer implementation.
+ void BrowserActionAdded(const Extension* extension, int index) {
+ [owner_ createActionButtonForExtension:extension withIndex:index];
+ [owner_ resizeContainerAndAnimate:NO];
+ }
+
+ void BrowserActionRemoved(const Extension* extension) {
+ [owner_ removeActionButtonForExtension:extension];
+ [owner_ resizeContainerAndAnimate:NO];
+ }
+
+ private:
+ // The object we need to inform when we get a notification. Weak. Owns us.
+ BrowserActionsController* owner_;
+
+ // Used for registering to receive notifications and automatic clean up.
+ NotificationRegistrar registrar_;
+
+ DISALLOW_COPY_AND_ASSIGN(ExtensionsServiceObserverBridge);
+};
+
+@implementation BrowserActionsController
+
+@synthesize containerView = containerView_;
+
+#pragma mark -
+#pragma mark Public Methods
+
+- (id)initWithBrowser:(Browser*)browser
+ containerView:(BrowserActionsContainerView*)container {
+ DCHECK(browser && container);
+
+ if ((self = [super init])) {
+ browser_ = browser;
+ profile_ = browser->profile();
+
+ if (!profile_->GetPrefs()->FindPreference(
+ prefs::kBrowserActionContainerWidth))
+ [BrowserActionsController registerUserPrefs:profile_->GetPrefs()];
+
+ observer_.reset(new ExtensionsServiceObserverBridge(self, profile_));
+ ExtensionsService* extensionsService = profile_->GetExtensionsService();
+ // |extensionsService| can be NULL in Incognito.
+ if (extensionsService) {
+ toolbarModel_ = extensionsService->toolbar_model();
+ toolbarModel_->AddObserver(observer_.get());
+ }
+
+ containerView_ = container;
+ [containerView_ setPostsFrameChangedNotifications:YES];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(containerFrameChanged:)
+ name:NSViewFrameDidChangeNotification
+ object:containerView_];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(containerDragStart:)
+ name:kBrowserActionGrippyDragStartedNotification
+ object:containerView_];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(containerDragging:)
+ name:kBrowserActionGrippyDraggingNotification
+ object:containerView_];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(containerDragFinished:)
+ name:kBrowserActionGrippyDragFinishedNotification
+ object:containerView_];
+ // Listen for a finished drag from any button to make sure each open window
+ // stays in sync.
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(actionButtonDragFinished:)
+ name:kBrowserActionButtonDragEndNotification
+ object:nil];
+
+ chevronAnimation_.reset([[NSViewAnimation alloc] init]);
+ [chevronAnimation_ gtm_setDuration:kAnimationDuration
+ eventMask:NSLeftMouseUpMask];
+ [chevronAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
+
+ hiddenButtons_.reset([[NSMutableArray alloc] init]);
+ buttons_.reset([[NSMutableDictionary alloc] init]);
+ [self createButtons];
+ [self showChevronIfNecessaryInFrame:[containerView_ frame] animate:NO];
+ [self updateGrippyCursors];
+ [container setResizable:!profile_->IsOffTheRecord()];
+ }
+
+ return self;
+}
+
+- (void)dealloc {
+ if (toolbarModel_)
+ toolbarModel_->RemoveObserver(observer_.get());
+
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+- (void)update {
+ for (BrowserActionButton* button in [buttons_ allValues]) {
+ [button setTabId:[self currentTabId]];
+ [button updateState];
+ }
+}
+
+- (NSUInteger)buttonCount {
+ return [buttons_ count];
+}
+
+- (NSUInteger)visibleButtonCount {
+ return [self buttonCount] - [hiddenButtons_ count];
+}
+
+- (MenuButton*)chevronMenuButton {
+ return chevronMenuButton_.get();
+}
+
+- (void)resizeContainerAndAnimate:(BOOL)animate {
+ int iconCount = toolbarModel_->GetVisibleIconCount();
+ if (iconCount < 0) // If no buttons are hidden.
+ iconCount = [self buttonCount];
+
+ [containerView_ resizeToWidth:[self containerWidthWithButtonCount:iconCount]
+ animate:animate];
+ NSRect frame = animate ? [containerView_ animationEndFrame] :
+ [containerView_ frame];
+
+ [self showChevronIfNecessaryInFrame:frame animate:animate];
+
+ if (!animate) {
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kBrowserActionVisibilityChangedNotification
+ object:self];
+ }
+}
+
+- (NSView*)browserActionViewForExtension:(const Extension*)extension {
+ for (BrowserActionButton* button in [buttons_ allValues]) {
+ if ([button extension] == extension)
+ return button;
+ }
+ NOTREACHED();
+ return nil;
+}
+
+- (CGFloat)savedWidth {
+ if (!toolbarModel_)
+ return 0;
+ if (!profile_->GetPrefs()->HasPrefPath(prefs::kExtensionToolbarSize)) {
+ // Migration code to the new VisibleIconCount pref.
+ // TODO(mpcomplete): remove this at some point.
+ double predefinedWidth =
+ profile_->GetPrefs()->GetReal(prefs::kBrowserActionContainerWidth);
+ if (predefinedWidth != 0) {
+ int iconWidth = kBrowserActionWidth + kBrowserActionButtonPadding;
+ int extraWidth = kChevronWidth;
+ toolbarModel_->SetVisibleIconCount(
+ (predefinedWidth - extraWidth) / iconWidth);
+ }
+ }
+
+ int savedButtonCount = toolbarModel_->GetVisibleIconCount();
+ if (savedButtonCount < 0 || // all icons are visible
+ static_cast<NSUInteger>(savedButtonCount) > [self buttonCount])
+ savedButtonCount = [self buttonCount];
+ return [self containerWidthWithButtonCount:savedButtonCount];
+}
+
+- (NSPoint)popupPointForBrowserAction:(const Extension*)extension {
+ if (!extension->browser_action())
+ return NSZeroPoint;
+
+ NSButton* button = [self buttonForExtension:extension];
+ if (!button)
+ return NSZeroPoint;
+
+ if ([hiddenButtons_ containsObject:button])
+ button = chevronMenuButton_.get();
+
+ // Anchor point just above the center of the bottom.
+ const NSRect bounds = [button bounds];
+ DCHECK([button isFlipped]);
+ NSPoint anchor = NSMakePoint(NSMidX(bounds),
+ NSMaxY(bounds) - kBrowserActionBubbleYOffset);
+ return [button convertPoint:anchor toView:nil];
+}
+
+- (BOOL)chevronIsHidden {
+ if (!chevronMenuButton_.get())
+ return YES;
+
+ if (![chevronAnimation_ isAnimating])
+ return [chevronMenuButton_ isHidden];
+
+ DCHECK([[chevronAnimation_ viewAnimations] count] > 0);
+
+ // The chevron is animating in or out. Determine which one and have the return
+ // value reflect where the animation is headed.
+ NSString* effect = [[[chevronAnimation_ viewAnimations] objectAtIndex:0]
+ valueForKey:NSViewAnimationEffectKey];
+ if (effect == NSViewAnimationFadeInEffect) {
+ return NO;
+ } else if (effect == NSViewAnimationFadeOutEffect) {
+ return YES;
+ }
+
+ NOTREACHED();
+ return YES;
+}
+
++ (void)registerUserPrefs:(PrefService*)prefs {
+ prefs->RegisterRealPref(prefs::kBrowserActionContainerWidth, 0);
+}
+
+#pragma mark -
+#pragma mark Private Methods
+
+- (void)createButtons {
+ if (!toolbarModel_)
+ return;
+
+ NSUInteger i = 0;
+ for (ExtensionList::iterator iter = toolbarModel_->begin();
+ iter != toolbarModel_->end(); ++iter) {
+ if (![self shouldDisplayBrowserAction:*iter])
+ continue;
+
+ [self createActionButtonForExtension:*iter withIndex:i++];
+ }
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(actionButtonUpdated:)
+ name:kBrowserActionButtonUpdatedNotification
+ object:nil];
+
+ CGFloat width = [self savedWidth];
+ [containerView_ resizeToWidth:width animate:NO];
+}
+
+- (void)createActionButtonForExtension:(const Extension*)extension
+ withIndex:(NSUInteger)index {
+ if (!extension->browser_action())
+ return;
+
+ if (![self shouldDisplayBrowserAction:extension])
+ return;
+
+ if (profile_->IsOffTheRecord())
+ index = toolbarModel_->OriginalIndexToIncognito(index);
+
+ // Show the container if it's the first button. Otherwise it will be shown
+ // already.
+ if ([self buttonCount] == 0)
+ [containerView_ setHidden:NO];
+
+ NSRect buttonFrame = NSMakeRect(0.0, kBrowserActionOriginYOffset,
+ kBrowserActionWidth, kBrowserActionHeight);
+ BrowserActionButton* newButton =
+ [[[BrowserActionButton alloc]
+ initWithFrame:buttonFrame
+ extension:extension
+ profile:profile_
+ tabId:[self currentTabId]] autorelease];
+ [newButton setTarget:self];
+ [newButton setAction:@selector(browserActionClicked:)];
+ NSString* buttonKey = base::SysUTF8ToNSString(extension->id());
+ if (!buttonKey)
+ return;
+ [buttons_ setObject:newButton forKey:buttonKey];
+
+ [self positionActionButtonsAndAnimate:NO];
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(actionButtonDragging:)
+ name:kBrowserActionButtonDraggingNotification
+ object:newButton];
+
+
+ [containerView_ setMaxWidth:
+ [self containerWidthWithButtonCount:[self buttonCount]]];
+ [containerView_ setNeedsDisplay:YES];
+}
+
+- (void)removeActionButtonForExtension:(const Extension*)extension {
+ if (!extension->browser_action())
+ return;
+
+ NSString* buttonKey = base::SysUTF8ToNSString(extension->id());
+ if (!buttonKey)
+ return;
+
+ BrowserActionButton* button = [buttons_ objectForKey:buttonKey];
+ // This could be the case in incognito, where only a subset of extensions are
+ // shown.
+ if (!button)
+ return;
+
+ [button removeFromSuperview];
+ // It may or may not be hidden, but it won't matter to NSMutableArray either
+ // way.
+ [hiddenButtons_ removeObject:button];
+ [self updateOverflowMenu];
+
+ [buttons_ removeObjectForKey:buttonKey];
+ if ([self buttonCount] == 0) {
+ // No more buttons? Hide the container.
+ [containerView_ setHidden:YES];
+ } else {
+ [self positionActionButtonsAndAnimate:NO];
+ }
+ [containerView_ setMaxWidth:
+ [self containerWidthWithButtonCount:[self buttonCount]]];
+ [containerView_ setNeedsDisplay:YES];
+}
+
+- (void)positionActionButtonsAndAnimate:(BOOL)animate {
+ NSUInteger i = 0;
+ for (ExtensionList::iterator iter = toolbarModel_->begin();
+ iter != toolbarModel_->end(); ++iter) {
+ if (![self shouldDisplayBrowserAction:*iter])
+ continue;
+ BrowserActionButton* button = [self buttonForExtension:(*iter)];
+ if (!button)
+ continue;
+ if (![button isBeingDragged])
+ [self moveButton:button toIndex:i animate:animate];
+ ++i;
+ }
+}
+
+- (void)updateButtonOpacity {
+ for (BrowserActionButton* button in [buttons_ allValues]) {
+ NSRect buttonFrame = [button frame];
+ if (NSContainsRect([containerView_ bounds], buttonFrame)) {
+ if ([button alphaValue] != 1.0)
+ [button setAlphaValue:1.0];
+
+ continue;
+ }
+ CGFloat intersectionWidth =
+ NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
+ CGFloat alpha = std::max(0.0f, intersectionWidth / NSWidth(buttonFrame));
+ [button setAlphaValue:alpha];
+ [button setNeedsDisplay:YES];
+ }
+}
+
+- (BrowserActionButton*)buttonForExtension:(const Extension*)extension {
+ NSString* extensionId = base::SysUTF8ToNSString(extension->id());
+ DCHECK(extensionId);
+ if (!extensionId)
+ return nil;
+ return [buttons_ objectForKey:extensionId];
+}
+
+- (CGFloat)containerWidthWithButtonCount:(NSUInteger)buttonCount {
+ // Left-side padding which works regardless of whether a button or
+ // chevron leads.
+ CGFloat width = kBrowserActionLeftPadding;
+
+ // Include the buttons and padding between.
+ if (buttonCount > 0) {
+ width += buttonCount * kBrowserActionWidth;
+ width += (buttonCount - 1) * kBrowserActionButtonPadding;
+ }
+
+ // Make room for the chevron if any buttons are hidden.
+ if ([self buttonCount] != [self visibleButtonCount]) {
+ // Chevron and buttons both include 1px padding w/in their bounds,
+ // so this leaves 2px between the last browser action and chevron,
+ // and also works right if the chevron is the only button.
+ width += kChevronWidth;
+ }
+
+ return width;
+}
+
+- (NSUInteger)containerButtonCapacity {
+ // Edge-to-edge span of the browser action buttons.
+ CGFloat actionSpan = [self savedWidth] - kBrowserActionLeftPadding;
+
+ // Add in some padding for the browser action on the end, then
+ // divide out to get the number of action buttons that fit.
+ return (actionSpan + kBrowserActionButtonPadding) /
+ (kBrowserActionWidth + kBrowserActionButtonPadding);
+}
+
+- (void)containerFrameChanged:(NSNotification*)notification {
+ [self updateButtonOpacity];
+ [[containerView_ window] invalidateCursorRectsForView:containerView_];
+ [self updateChevronPositionInFrame:[containerView_ frame]];
+}
+
+- (void)containerDragStart:(NSNotification*)notification {
+ [self setChevronHidden:YES inFrame:[containerView_ frame] animate:YES];
+ while([hiddenButtons_ count] > 0) {
+ [containerView_ addSubview:[hiddenButtons_ objectAtIndex:0]];
+ [hiddenButtons_ removeObjectAtIndex:0];
+ }
+}
+
+- (void)containerDragging:(NSNotification*)notification {
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kBrowserActionGrippyDraggingNotification
+ object:self];
+}
+
+- (void)containerDragFinished:(NSNotification*)notification {
+ for (ExtensionList::iterator iter = toolbarModel_->begin();
+ iter != toolbarModel_->end(); ++iter) {
+ BrowserActionButton* button = [self buttonForExtension:(*iter)];
+ NSRect buttonFrame = [button frame];
+ if (NSContainsRect([containerView_ bounds], buttonFrame))
+ continue;
+
+ CGFloat intersectionWidth =
+ NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
+ // Pad the threshold by 5 pixels in order to have the buttons hide more
+ // easily.
+ if (([containerView_ grippyPinned] && intersectionWidth > 0) ||
+ (intersectionWidth <= (NSWidth(buttonFrame) / 2) + 5.0)) {
+ [button setAlphaValue:0.0];
+ [button removeFromSuperview];
+ [hiddenButtons_ addObject:button];
+ }
+ }
+ [self updateOverflowMenu];
+ [self updateGrippyCursors];
+
+ if (!profile_->IsOffTheRecord())
+ toolbarModel_->SetVisibleIconCount([self visibleButtonCount]);
+
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kBrowserActionGrippyDragFinishedNotification
+ object:self];
+}
+
+- (void)actionButtonUpdated:(NSNotification*)notification {
+ BrowserActionButton* button = [notification object];
+ if (![hiddenButtons_ containsObject:button])
+ return;
+
+ // +1 item because of the title placeholder. See |updateOverflowMenu|.
+ NSUInteger menuIndex = [hiddenButtons_ indexOfObject:button] + 1;
+ NSMenuItem* item = [[chevronMenuButton_ attachedMenu] itemAtIndex:menuIndex];
+ DCHECK(button == [item representedObject]);
+ [item setImage:[button compositedImage]];
+}
+
+- (void)actionButtonDragging:(NSNotification*)notification {
+ if (![self chevronIsHidden])
+ [self setChevronHidden:YES inFrame:[containerView_ frame] animate:YES];
+
+ // Determine what index the dragged button should lie in, alter the model and
+ // reposition the buttons.
+ CGFloat dragThreshold = std::floor(kBrowserActionWidth / 2);
+ BrowserActionButton* draggedButton = [notification object];
+ NSRect draggedButtonFrame = [draggedButton frame];
+
+ NSUInteger index = 0;
+ for (ExtensionList::iterator iter = toolbarModel_->begin();
+ iter != toolbarModel_->end(); ++iter) {
+ BrowserActionButton* button = [self buttonForExtension:(*iter)];
+ CGFloat intersectionWidth =
+ NSWidth(NSIntersectionRect(draggedButtonFrame, [button frame]));
+
+ if (intersectionWidth > dragThreshold && button != draggedButton &&
+ ![button isAnimating] && index < [self visibleButtonCount]) {
+ toolbarModel_->MoveBrowserAction([draggedButton extension], index);
+ [self positionActionButtonsAndAnimate:YES];
+ return;
+ }
+ ++index;
+ }
+}
+
+- (void)actionButtonDragFinished:(NSNotification*)notification {
+ [self showChevronIfNecessaryInFrame:[containerView_ frame] animate:YES];
+ [self positionActionButtonsAndAnimate:YES];
+}
+
+- (void)moveButton:(BrowserActionButton*)button
+ toIndex:(NSUInteger)index
+ animate:(BOOL)animate {
+ CGFloat xOffset = kBrowserActionLeftPadding +
+ (index * (kBrowserActionWidth + kBrowserActionButtonPadding));
+ NSRect buttonFrame = [button frame];
+ buttonFrame.origin.x = xOffset;
+ [button setFrame:buttonFrame animate:animate];
+
+ if (index < [self containerButtonCapacity]) {
+ // Make sure the button is within the visible container.
+ if ([button superview] != containerView_) {
+ [containerView_ addSubview:button];
+ [button setAlphaValue:1.0];
+ [hiddenButtons_ removeObjectIdenticalTo:button];
+ }
+ } else if (![hiddenButtons_ containsObject:button]) {
+ [hiddenButtons_ addObject:button];
+ [button removeFromSuperview];
+ [button setAlphaValue:0.0];
+ [self updateOverflowMenu];
+ }
+}
+
+- (void)browserActionClicked:(BrowserActionButton*)button {
+ int tabId = [self currentTabId];
+ if (tabId < 0) {
+ NOTREACHED() << "No current tab.";
+ return;
+ }
+
+ ExtensionAction* action = [button extension]->browser_action();
+ if (action->HasPopup(tabId)) {
+ GURL popupUrl = action->GetPopupUrl(tabId);
+ // If a popup is already showing, check if the popup URL is the same. If so,
+ // then close the popup.
+ ExtensionPopupController* popup = [ExtensionPopupController popup];
+ if (popup &&
+ [[popup window] isVisible] &&
+ [popup extensionHost]->GetURL() == popupUrl) {
+ [popup close];
+ return;
+ }
+ NSPoint arrowPoint = [self popupPointForBrowserAction:[button extension]];
+ [ExtensionPopupController showURL:popupUrl
+ inBrowser:browser_
+ anchoredAt:arrowPoint
+ arrowLocation:info_bubble::kTopRight
+ devMode:NO];
+ } else {
+ ExtensionBrowserEventRouter::GetInstance()->BrowserActionExecuted(
+ profile_, action->extension_id(), browser_);
+ }
+}
+
+- (BOOL)shouldDisplayBrowserAction:(const Extension*)extension {
+ // Only display incognito-enabled extensions while in incognito mode.
+ return (!profile_->IsOffTheRecord() ||
+ profile_->GetExtensionsService()->IsIncognitoEnabled(extension));
+}
+
+- (void)showChevronIfNecessaryInFrame:(NSRect)frame animate:(BOOL)animate {
+ [self setChevronHidden:([self buttonCount] == [self visibleButtonCount])
+ inFrame:frame
+ animate:animate];
+}
+
+- (void)updateChevronPositionInFrame:(NSRect)frame {
+ CGFloat xPos = NSWidth(frame) - kChevronWidth;
+ NSRect buttonFrame = NSMakeRect(xPos,
+ kBrowserActionOriginYOffset,
+ kChevronWidth,
+ kBrowserActionHeight);
+ [chevronMenuButton_ setFrame:buttonFrame];
+}
+
+- (void)setChevronHidden:(BOOL)hidden
+ inFrame:(NSRect)frame
+ animate:(BOOL)animate {
+ if (hidden == [self chevronIsHidden])
+ return;
+
+ if (!chevronMenuButton_.get()) {
+ chevronMenuButton_.reset([[ChevronMenuButton alloc] init]);
+ [chevronMenuButton_ setBordered:NO];
+ [chevronMenuButton_ setShowsBorderOnlyWhileMouseInside:YES];
+ NSImage* chevronImage = nsimage_cache::ImageNamed(kOverflowChevronsName);
+ [chevronMenuButton_ setImage:chevronImage];
+ [containerView_ addSubview:chevronMenuButton_];
+ }
+
+ if (!hidden)
+ [self updateOverflowMenu];
+
+ [self updateChevronPositionInFrame:frame];
+
+ // Stop any running animation.
+ [chevronAnimation_ stopAnimation];
+
+ if (!animate) {
+ [chevronMenuButton_ setHidden:hidden];
+ return;
+ }
+
+ NSDictionary* animationDictionary;
+ if (hidden) {
+ animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
+ chevronMenuButton_.get(), NSViewAnimationTargetKey,
+ NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey,
+ nil];
+ } else {
+ [chevronMenuButton_ setHidden:NO];
+ animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
+ chevronMenuButton_.get(), NSViewAnimationTargetKey,
+ NSViewAnimationFadeInEffect, NSViewAnimationEffectKey,
+ nil];
+ }
+ [chevronAnimation_ setViewAnimations:
+ [NSArray arrayWithObject:animationDictionary]];
+ [chevronAnimation_ startAnimation];
+}
+
+- (void)chevronItemSelected:(id)menuItem {
+ [self browserActionClicked:[menuItem representedObject]];
+}
+
+- (void)updateOverflowMenu {
+ overflowMenu_.reset([[NSMenu alloc] initWithTitle:@""]);
+ // See menu_button.h for documentation on why this is needed.
+ [overflowMenu_ addItemWithTitle:@"" action:nil keyEquivalent:@""];
+
+ for (BrowserActionButton* button in hiddenButtons_.get()) {
+ NSString* name = base::SysUTF8ToNSString([button extension]->name());
+ NSMenuItem* item =
+ [overflowMenu_ addItemWithTitle:name
+ action:@selector(chevronItemSelected:)
+ keyEquivalent:@""];
+ [item setRepresentedObject:button];
+ [item setImage:[button compositedImage]];
+ [item setTarget:self];
+ }
+ [chevronMenuButton_ setAttachedMenu:overflowMenu_];
+}
+
+- (void)updateGrippyCursors {
+ [containerView_ setCanDragLeft:[hiddenButtons_ count] > 0];
+ [containerView_ setCanDragRight:[self visibleButtonCount] > 0];
+ [[containerView_ window] invalidateCursorRectsForView:containerView_];
+}
+
+- (int)currentTabId {
+ TabContents* selected_tab = browser_->GetSelectedTabContents();
+ if (!selected_tab)
+ return -1;
+
+ return selected_tab->controller().session_id().id();
+}
+
+#pragma mark -
+#pragma mark Testing Methods
+
+- (NSButton*)buttonWithIndex:(NSUInteger)index {
+ if (profile_->IsOffTheRecord())
+ index = toolbarModel_->IncognitoIndexToOriginal(index);
+ if (index < toolbarModel_->size()) {
+ const Extension* extension = toolbarModel_->GetExtensionByIndex(index);
+ return [buttons_ objectForKey:base::SysUTF8ToNSString(extension->id())];
+ }
+ return nil;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/extensions/chevron_menu_button.h b/chrome/browser/ui/cocoa/extensions/chevron_menu_button.h
new file mode 100644
index 0000000..86c8209
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/chevron_menu_button.h
@@ -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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_EXTENSIONS_CHEVRON_MENU_BUTTON_H_
+#define CHROME_BROWSER_UI_COCOA_EXTENSIONS_CHEVRON_MENU_BUTTON_H_
+#pragma once
+
+#import "chrome/browser/ui/cocoa/menu_button.h"
+
+@interface ChevronMenuButton : MenuButton {
+}
+
+// Overrides cell class with |ChevronMenuButtonCell|.
++ (Class)cellClass;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_EXTENSIONS_CHEVRON_MENU_BUTTON_H_
diff --git a/chrome/browser/ui/cocoa/extensions/chevron_menu_button.mm b/chrome/browser/ui/cocoa/extensions/chevron_menu_button.mm
new file mode 100644
index 0000000..04f1506
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/chevron_menu_button.mm
@@ -0,0 +1,15 @@
+// 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 "chrome/browser/ui/cocoa/extensions/chevron_menu_button.h"
+
+#include "chrome/browser/ui/cocoa/extensions/chevron_menu_button_cell.h"
+
+@implementation ChevronMenuButton
+
++ (Class)cellClass {
+ return [ChevronMenuButtonCell class];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/extensions/chevron_menu_button_cell.h b/chrome/browser/ui/cocoa/extensions/chevron_menu_button_cell.h
new file mode 100644
index 0000000..429015c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/chevron_menu_button_cell.h
@@ -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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_EXTENSIONS_CHEVRON_MENU_BUTTON_CELL_H_
+#define CHROME_BROWSER_UI_COCOA_EXTENSIONS_CHEVRON_MENU_BUTTON_CELL_H_
+#pragma once
+
+#import "chrome/browser/ui/cocoa/clickhold_button_cell.h"
+
+@interface ChevronMenuButtonCell : ClickHoldButtonCell {
+}
+
+// Adds a gradient border to the RHS of the cell when not hovered.
+- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_EXTENSIONS_CHEVRON_MENU_BUTTON_CELL_H_
diff --git a/chrome/browser/ui/cocoa/extensions/chevron_menu_button_cell.mm b/chrome/browser/ui/cocoa/extensions/chevron_menu_button_cell.mm
new file mode 100644
index 0000000..8d4408a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/chevron_menu_button_cell.mm
@@ -0,0 +1,47 @@
+// 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 "chrome/browser/ui/cocoa/extensions/chevron_menu_button_cell.h"
+
+namespace {
+
+// Width of the divider.
+const CGFloat kDividerWidth = 1.0;
+
+// Vertical inset from edge of cell to divider start.
+const CGFloat kDividerInset = 3.0;
+
+// Grayscale for the center of the divider.
+const CGFloat kDividerGrayscale = 0.5;
+
+} // namespace
+
+@implementation ChevronMenuButtonCell
+
+- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
+ [super drawWithFrame:cellFrame inView:controlView];
+
+ if ([self isMouseInside])
+ return;
+
+ NSColor* middleColor =
+ [NSColor colorWithCalibratedWhite:kDividerGrayscale alpha:1.0];
+ NSColor* endPointColor = [middleColor colorWithAlphaComponent:0.0];
+
+ // Blend from background to |kDividerGrayscale| and back to
+ // background.
+ scoped_nsobject<NSGradient> borderGradient([[NSGradient alloc]
+ initWithColorsAndLocations:endPointColor, (CGFloat)0.0,
+ middleColor, (CGFloat)0.5,
+ endPointColor, (CGFloat)1.0,
+ nil]);
+
+ NSRect edgeRect, remainder;
+ NSDivideRect(cellFrame, &edgeRect, &remainder, kDividerWidth, NSMaxXEdge);
+ edgeRect = NSInsetRect(edgeRect, 0.0, kDividerInset);
+
+ [borderGradient drawInRect:edgeRect angle:90.0];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/extensions/chevron_menu_button_unittest.mm b/chrome/browser/ui/cocoa/extensions/chevron_menu_button_unittest.mm
new file mode 100644
index 0000000..71c8929
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/chevron_menu_button_unittest.mm
@@ -0,0 +1,50 @@
+// 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/ui/cocoa/extensions/chevron_menu_button.h"
+#import "chrome/browser/ui/cocoa/extensions/chevron_menu_button_cell.h"
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+
+namespace {
+
+class ChevronMenuButtonTest : public CocoaTest {
+ public:
+ ChevronMenuButtonTest() {
+ NSRect frame = NSMakeRect(0, 0, 50, 30);
+ scoped_nsobject<ChevronMenuButton> button(
+ [[ChevronMenuButton alloc] initWithFrame:frame]);
+ button_ = button.get();
+ [[test_window() contentView] addSubview:button_];
+ }
+
+ ChevronMenuButton* button_;
+};
+
+// Test basic view operation.
+TEST_VIEW(ChevronMenuButtonTest, button_);
+
+// |ChevronMenuButton exists entirely to override the cell class.
+TEST_F(ChevronMenuButtonTest, CellSubclass) {
+ EXPECT_TRUE([[button_ cell] isKindOfClass:[ChevronMenuButtonCell class]]);
+}
+
+// Test both hovered and non-hovered display.
+TEST_F(ChevronMenuButtonTest, HoverAndNonHoverDisplay) {
+ ChevronMenuButtonCell* cell = [button_ cell];
+ EXPECT_FALSE([cell showsBorderOnlyWhileMouseInside]);
+ EXPECT_FALSE([cell isMouseInside]);
+
+ [cell setShowsBorderOnlyWhileMouseInside:YES];
+ [cell mouseEntered:nil];
+ EXPECT_TRUE([cell isMouseInside]);
+ [button_ display];
+
+ [cell mouseExited:nil];
+ EXPECT_FALSE([cell isMouseInside]);
+ [button_ display];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/extensions/extension_action_context_menu.h b/chrome/browser/ui/cocoa/extensions/extension_action_context_menu.h
new file mode 100644
index 0000000..e40388f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/extension_action_context_menu.h
@@ -0,0 +1,62 @@
+// 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_UI_COCOA_EXTENSIONS_EXTENSION_ACTION_CONTEXT_MENU_H_
+#define CHROME_BROWSER_UI_COCOA_EXTENSIONS_EXTENSION_ACTION_CONTEXT_MENU_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_ptr.h"
+#include "base/scoped_nsobject.h"
+
+class AsyncUninstaller;
+class DevmodeObserver;
+class Extension;
+class ExtensionAction;
+class NotificationRegistrar;
+class Profile;
+
+namespace extension_action_context_menu {
+
+class DevmodeObserver;
+
+} // namespace extension_action_context_menu
+
+// A context menu used by any extension UI components that require it.
+@interface ExtensionActionContextMenu : NSMenu {
+ @private
+ // The extension that this menu belongs to. Weak.
+ const Extension* extension_;
+
+ // The extension action this menu belongs to. Weak.
+ ExtensionAction* action_;
+
+ // The browser profile of the window that contains this extension. Weak.
+ Profile* profile_;
+
+ // The inspector menu item. Need to keep this around to add and remove it.
+ scoped_nsobject<NSMenuItem> inspectorItem_;
+
+ // The observer used to listen for pref changed notifications.
+ scoped_ptr<extension_action_context_menu::DevmodeObserver> observer_;
+
+ // Used to load the extension icon asynchronously on the I/O thread then show
+ // the uninstall confirmation dialog.
+ scoped_ptr<AsyncUninstaller> uninstaller_;
+}
+
+// Initializes and returns a context menu for the given extension and profile.
+- (id)initWithExtension:(const Extension*)extension
+ profile:(Profile*)profile
+ extensionAction:(ExtensionAction*)action;
+
+// Show or hide the inspector menu item.
+- (void)updateInspectorItem;
+
+@end
+
+typedef ExtensionActionContextMenu ExtensionActionContextMenuMac;
+
+#endif // CHROME_BROWSER_UI_COCOA_EXTENSIONS_EXTENSION_ACTION_CONTEXT_MENU_H_
diff --git a/chrome/browser/ui/cocoa/extensions/extension_action_context_menu.mm b/chrome/browser/ui/cocoa/extensions/extension_action_context_menu.mm
new file mode 100644
index 0000000..df25dba
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/extension_action_context_menu.mm
@@ -0,0 +1,278 @@
+// 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/ui/cocoa/extensions/extension_action_context_menu.h"
+
+#include "app/l10n_util_mac.h"
+#include "base/sys_string_conversions.h"
+#include "base/task.h"
+#include "chrome/browser/browser_list.h"
+#include "chrome/browser/extensions/extension_install_ui.h"
+#include "chrome/browser/extensions/extensions_service.h"
+#include "chrome/browser/extensions/extension_tabs_module.h"
+#include "chrome/browser/prefs/pref_change_registrar.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/ui/cocoa/browser_window_cocoa.h"
+#include "chrome/browser/ui/cocoa/browser_window_controller.h"
+#include "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h"
+#include "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
+#include "chrome/browser/ui/cocoa/info_bubble_view.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
+#include "chrome/browser/ui/cocoa/toolbar_controller.h"
+#include "chrome/common/extensions/extension.h"
+#include "chrome/common/extensions/extension_action.h"
+#include "chrome/common/extensions/extension_constants.h"
+#include "chrome/common/notification_details.h"
+#include "chrome/common/notification_observer.h"
+#include "chrome/common/notification_type.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/url_constants.h"
+#include "grit/generated_resources.h"
+
+// A class that loads the extension icon on the I/O thread before showing the
+// confirmation dialog to uninstall the given extension.
+// Also acts as the extension's UI delegate in order to display the dialog.
+class AsyncUninstaller : public ExtensionInstallUI::Delegate {
+ public:
+ AsyncUninstaller(const Extension* extension, Profile* profile)
+ : extension_(extension),
+ profile_(profile) {
+ install_ui_.reset(new ExtensionInstallUI(profile));
+ install_ui_->ConfirmUninstall(this, extension_);
+ }
+
+ ~AsyncUninstaller() {}
+
+ // Overridden by ExtensionInstallUI::Delegate.
+ virtual void InstallUIProceed() {
+ profile_->GetExtensionsService()->
+ UninstallExtension(extension_->id(), false);
+ }
+
+ virtual void InstallUIAbort() {}
+
+ private:
+ // The extension that we're loading the icon for. Weak.
+ const Extension* extension_;
+
+ // The current profile. Weak.
+ Profile* profile_;
+
+ scoped_ptr<ExtensionInstallUI> install_ui_;
+
+ DISALLOW_COPY_AND_ASSIGN(AsyncUninstaller);
+};
+
+namespace extension_action_context_menu {
+
+class DevmodeObserver : public NotificationObserver {
+ public:
+ DevmodeObserver(ExtensionActionContextMenu* menu,
+ PrefService* service)
+ : menu_(menu), pref_service_(service) {
+ registrar_.Init(pref_service_);
+ registrar_.Add(prefs::kExtensionsUIDeveloperMode, this);
+ }
+ virtual ~DevmodeObserver() {}
+
+ void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ if (type == NotificationType::PREF_CHANGED)
+ [menu_ updateInspectorItem];
+ else
+ NOTREACHED();
+ }
+
+ private:
+ ExtensionActionContextMenu* menu_;
+ PrefService* pref_service_;
+ PrefChangeRegistrar registrar_;
+};
+
+} // namespace extension_action_context_menu
+
+@interface ExtensionActionContextMenu(Private)
+// Callback for the context menu items.
+- (void)dispatch:(id)menuItem;
+@end
+
+@implementation ExtensionActionContextMenu
+
+namespace {
+// Enum of menu item choices to their respective indices.
+// NOTE: You MUST keep this in sync with the |menuItems| NSArray below.
+enum {
+ kExtensionContextName = 0,
+ kExtensionContextOptions = 2,
+ kExtensionContextDisable = 3,
+ kExtensionContextUninstall = 4,
+ kExtensionContextManage = 6,
+ kExtensionContextInspect = 7
+};
+
+int CurrentTabId() {
+ Browser* browser = BrowserList::GetLastActive();
+ if(!browser)
+ return -1;
+ TabContents* contents = browser->GetSelectedTabContents();
+ if (!contents)
+ return -1;
+ return ExtensionTabUtil::GetTabId(contents);
+}
+
+} // namespace
+
+- (id)initWithExtension:(const Extension*)extension
+ profile:(Profile*)profile
+ extensionAction:(ExtensionAction*)action{
+ if ((self = [super initWithTitle:@""])) {
+ action_ = action;
+ extension_ = extension;
+ profile_ = profile;
+
+ NSArray* menuItems = [NSArray arrayWithObjects:
+ base::SysUTF8ToNSString(extension->name()),
+ [NSMenuItem separatorItem],
+ l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_OPTIONS),
+ l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_DISABLE),
+ l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_UNINSTALL),
+ [NSMenuItem separatorItem],
+ l10n_util::GetNSStringWithFixup(IDS_MANAGE_EXTENSIONS),
+ nil];
+
+ for (id item in menuItems) {
+ if ([item isKindOfClass:[NSMenuItem class]]) {
+ [self addItem:item];
+ } else if ([item isKindOfClass:[NSString class]]) {
+ NSMenuItem* itemObj = [self addItemWithTitle:item
+ action:@selector(dispatch:)
+ keyEquivalent:@""];
+ // The tag should correspond to the enum above.
+ // NOTE: The enum and the order of the menu items MUST be in sync.
+ [itemObj setTag:[self indexOfItem:itemObj]];
+
+ // Disable the 'Options' item if there are no options to set.
+ if ([itemObj tag] == kExtensionContextOptions &&
+ extension_->options_url().spec().length() <= 0) {
+ // Setting the target to nil will disable the item. For some reason
+ // setEnabled:NO does not work.
+ [itemObj setTarget:nil];
+ } else {
+ [itemObj setTarget:self];
+ }
+ }
+ }
+
+ NSString* inspectorTitle =
+ l10n_util::GetNSStringWithFixup(IDS_EXTENSION_ACTION_INSPECT_POPUP);
+ inspectorItem_.reset([[NSMenuItem alloc] initWithTitle:inspectorTitle
+ action:@selector(dispatch:)
+ keyEquivalent:@""]);
+ [inspectorItem_.get() setTarget:self];
+ [inspectorItem_.get() setTag:kExtensionContextInspect];
+
+ PrefService* service = profile_->GetPrefs();
+ observer_.reset(
+ new extension_action_context_menu::DevmodeObserver(self, service));
+
+ [self updateInspectorItem];
+ return self;
+ }
+ return nil;
+}
+
+- (void)updateInspectorItem {
+ PrefService* service = profile_->GetPrefs();
+ bool devmode = service->GetBoolean(prefs::kExtensionsUIDeveloperMode);
+ if (devmode) {
+ if ([self indexOfItem:inspectorItem_.get()] == -1)
+ [self addItem:inspectorItem_.get()];
+ } else {
+ if ([self indexOfItem:inspectorItem_.get()] != -1)
+ [self removeItem:inspectorItem_.get()];
+ }
+}
+
+- (void)dispatch:(id)menuItem {
+ Browser* browser = BrowserList::FindBrowserWithProfile(profile_);
+ if (!browser)
+ return;
+
+ NSMenuItem* item = (NSMenuItem*)menuItem;
+ switch ([item tag]) {
+ case kExtensionContextName: {
+ GURL url(std::string(extension_urls::kGalleryBrowsePrefix) +
+ std::string("/detail/") + extension_->id());
+ browser->OpenURL(url, GURL(), NEW_FOREGROUND_TAB, PageTransition::LINK);
+ break;
+ }
+ case kExtensionContextOptions: {
+ DCHECK(!extension_->options_url().is_empty());
+ profile_->GetExtensionProcessManager()->OpenOptionsPage(extension_,
+ browser);
+ break;
+ }
+ case kExtensionContextDisable: {
+ ExtensionsService* extensionService = profile_->GetExtensionsService();
+ if (!extensionService)
+ return; // Incognito mode.
+ extensionService->DisableExtension(extension_->id());
+ break;
+ }
+ case kExtensionContextUninstall: {
+ uninstaller_.reset(new AsyncUninstaller(extension_, profile_));
+ break;
+ }
+ case kExtensionContextManage: {
+ browser->OpenURL(GURL(chrome::kChromeUIExtensionsURL), GURL(),
+ NEW_FOREGROUND_TAB, PageTransition::LINK);
+ break;
+ }
+ case kExtensionContextInspect: {
+ BrowserWindowCocoa* window =
+ static_cast<BrowserWindowCocoa*>(browser->window());
+ ToolbarController* toolbarController =
+ [window->cocoa_controller() toolbarController];
+ LocationBarViewMac* locationBarView =
+ [toolbarController locationBarBridge];
+
+ NSPoint popupPoint = NSZeroPoint;
+ if (extension_->page_action() == action_) {
+ popupPoint = locationBarView->GetPageActionBubblePoint(action_);
+
+ } else if (extension_->browser_action() == action_) {
+ BrowserActionsController* controller =
+ [toolbarController browserActionsController];
+ popupPoint = [controller popupPointForBrowserAction:extension_];
+
+ } else {
+ NOTREACHED() << "action_ is not a page action or browser action?";
+ }
+
+ int tabId = CurrentTabId();
+ GURL url = action_->GetPopupUrl(tabId);
+ DCHECK(url.is_valid());
+ [ExtensionPopupController showURL:url
+ inBrowser:BrowserList::GetLastActive()
+ anchoredAt:popupPoint
+ arrowLocation:info_bubble::kTopRight
+ devMode:YES];
+ break;
+ }
+ default:
+ NOTREACHED();
+ break;
+ }
+}
+
+- (BOOL)validateMenuItem:(NSMenuItem*)menuItem {
+ if([menuItem isEqualTo:inspectorItem_.get()]) {
+ return action_ && action_->HasPopup(CurrentTabId());
+ }
+ return YES;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/extensions/extension_infobar_controller.h b/chrome/browser/ui/cocoa/extensions/extension_infobar_controller.h
new file mode 100644
index 0000000..b2c9a8e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/extension_infobar_controller.h
@@ -0,0 +1,41 @@
+// 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_UI_COCOA_EXTENSIONS_EXTENSION_INFOBAR_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_EXTENSIONS_EXTENSION_INFOBAR_CONTROLLER_H_
+#pragma once
+
+#import "chrome/browser/ui/cocoa/infobar_controller.h"
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+
+@class ExtensionActionContextMenu;
+class ExtensionInfoBarDelegate;
+class InfobarBridge;
+@class MenuButton;
+
+@interface ExtensionInfoBarController : InfoBarController {
+ // The native extension view retrieved from the extension host. Weak.
+ NSView* extensionView_;
+
+ // The window containing this InfoBar. Weak.
+ NSWindow* window_;
+
+ // The InfoBar's button with the Extension's icon that launches the context
+ // menu.
+ scoped_nsobject<MenuButton> dropdownButton_;
+
+ // The context menu that pops up when the left button is clicked.
+ scoped_nsobject<ExtensionActionContextMenu> contextMenu_;
+
+ // Helper class to bridge C++ and ObjC functionality together for the infobar.
+ scoped_ptr<InfobarBridge> bridge_;
+}
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_EXTENSIONS_EXTENSION_INFOBAR_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/extensions/extension_infobar_controller.mm b/chrome/browser/ui/cocoa/extensions/extension_infobar_controller.mm
new file mode 100644
index 0000000..1214370
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/extension_infobar_controller.mm
@@ -0,0 +1,266 @@
+// 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/ui/cocoa/extensions/extension_infobar_controller.h"
+
+#include <cmath>
+
+#include "app/resource_bundle.h"
+#include "chrome/browser/extensions/extension_host.h"
+#include "chrome/browser/extensions/extension_infobar_delegate.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#import "chrome/browser/ui/cocoa/animatable_view.h"
+#import "chrome/browser/ui/cocoa/extensions/extension_action_context_menu.h"
+#import "chrome/browser/ui/cocoa/menu_button.h"
+#include "chrome/browser/ui/cocoa/infobar.h"
+#include "chrome/common/extensions/extension.h"
+#include "chrome/common/extensions/extension_icon_set.h"
+#include "chrome/common/extensions/extension_resource.h"
+#include "gfx/canvas_skia.h"
+#include "grit/theme_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+
+namespace {
+const CGFloat kAnimationDuration = 0.12;
+const CGFloat kBottomBorderHeightPx = 1.0;
+const CGFloat kButtonHeightPx = 26.0;
+const CGFloat kButtonLeftMarginPx = 2.0;
+const CGFloat kButtonWidthPx = 34.0;
+const CGFloat kDropArrowLeftMarginPx = 3.0;
+const CGFloat kToolbarMinHeightPx = 36.0;
+const CGFloat kToolbarMaxHeightPx = 72.0;
+} // namespace
+
+@interface ExtensionInfoBarController(Private)
+// Called when the extension's hosted NSView has been resized.
+- (void)extensionViewFrameChanged;
+// Returns the clamped height of the extension view to be within the min and max
+// values defined above.
+- (CGFloat)clampedExtensionViewHeight;
+// Adjusts the width of the extension's hosted view to match the window's width
+// and sets the proper height for it as well.
+- (void)adjustExtensionViewSize;
+// Sets the image to be used in the button on the left side of the infobar.
+- (void)setButtonImage:(NSImage*)image;
+@end
+
+// A helper class to bridge the asynchronous Skia bitmap loading mechanism to
+// the extension's button.
+class InfobarBridge : public ExtensionInfoBarDelegate::DelegateObserver,
+ public ImageLoadingTracker::Observer {
+ public:
+ explicit InfobarBridge(ExtensionInfoBarController* owner)
+ : owner_(owner),
+ delegate_([owner delegate]->AsExtensionInfoBarDelegate()),
+ tracker_(this) {
+ delegate_->set_observer(this);
+ LoadIcon();
+ }
+
+ virtual ~InfobarBridge() {
+ if (delegate_)
+ delegate_->set_observer(NULL);
+ }
+
+ // Load the Extension's icon image.
+ void LoadIcon() {
+ const Extension* extension = delegate_->extension_host()->extension();
+ ExtensionResource icon_resource = extension->GetIconResource(
+ Extension::EXTENSION_ICON_BITTY, ExtensionIconSet::MATCH_EXACTLY);
+ if (!icon_resource.relative_path().empty()) {
+ tracker_.LoadImage(extension, icon_resource,
+ gfx::Size(Extension::EXTENSION_ICON_BITTY,
+ Extension::EXTENSION_ICON_BITTY),
+ ImageLoadingTracker::DONT_CACHE);
+ } else {
+ OnImageLoaded(NULL, icon_resource, 0);
+ }
+ }
+
+ // ImageLoadingTracker::Observer implementation.
+ // TODO(andybons): The infobar view implementations share a lot of the same
+ // code. Come up with a strategy to share amongst them.
+ virtual void OnImageLoaded(
+ SkBitmap* image, ExtensionResource resource, int index) {
+ if (!delegate_)
+ return; // The delegate can go away while the image asynchronously loads.
+
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+
+ // Fall back on the default extension icon on failure.
+ SkBitmap* icon;
+ if (!image || image->empty())
+ icon = rb.GetBitmapNamed(IDR_EXTENSIONS_SECTION);
+ else
+ icon = image;
+
+ SkBitmap* drop_image = rb.GetBitmapNamed(IDR_APP_DROPARROW);
+
+ const int image_size = Extension::EXTENSION_ICON_BITTY;
+ scoped_ptr<gfx::CanvasSkia> canvas(
+ new gfx::CanvasSkia(
+ image_size + kDropArrowLeftMarginPx + drop_image->width(),
+ image_size, false));
+ canvas->DrawBitmapInt(*icon,
+ 0, 0, icon->width(), icon->height(),
+ 0, 0, image_size, image_size,
+ false);
+ canvas->DrawBitmapInt(*drop_image,
+ image_size + kDropArrowLeftMarginPx,
+ image_size / 2);
+ [owner_ setButtonImage:gfx::SkBitmapToNSImage(canvas->ExtractBitmap())];
+ }
+
+ // Overridden from ExtensionInfoBarDelegate::DelegateObserver:
+ virtual void OnDelegateDeleted() {
+ delegate_ = NULL;
+ }
+
+ private:
+ // Weak. Owns us.
+ ExtensionInfoBarController* owner_;
+
+ // Weak.
+ ExtensionInfoBarDelegate* delegate_;
+
+ // Loads the extensions's icon on the file thread.
+ ImageLoadingTracker tracker_;
+
+ DISALLOW_COPY_AND_ASSIGN(InfobarBridge);
+};
+
+
+@implementation ExtensionInfoBarController
+
+- (id)initWithDelegate:(InfoBarDelegate*)delegate
+ window:(NSWindow*)window {
+ if ((self = [super initWithDelegate:delegate])) {
+ window_ = window;
+ dropdownButton_.reset([[MenuButton alloc] init]);
+
+ ExtensionHost* extensionHost = delegate_->AsExtensionInfoBarDelegate()->
+ extension_host();
+ contextMenu_.reset([[ExtensionActionContextMenu alloc]
+ initWithExtension:extensionHost->extension()
+ profile:extensionHost->profile()
+ extensionAction:NULL]);
+ // See menu_button.h for documentation on why this is needed.
+ NSMenuItem* dummyItem =
+ [[[NSMenuItem alloc] initWithTitle:@""
+ action:nil
+ keyEquivalent:@""] autorelease];
+ [contextMenu_ insertItem:dummyItem atIndex:0];
+ [dropdownButton_ setAttachedMenu:contextMenu_.get()];
+
+ bridge_.reset(new InfobarBridge(self));
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+- (void)addAdditionalControls {
+ [self removeButtons];
+
+ extensionView_ = delegate_->AsExtensionInfoBarDelegate()->extension_host()->
+ view()->native_view();
+
+ // Add the extension's RenderWidgetHostViewMac to the view hierarchy of the
+ // InfoBar and make sure to place it below the Close button.
+ [infoBarView_ addSubview:extensionView_
+ positioned:NSWindowBelow
+ relativeTo:(NSView*)closeButton_];
+
+ // Add the context menu button to the hierarchy.
+ [dropdownButton_ setShowsBorderOnlyWhileMouseInside:YES];
+ CGFloat buttonY =
+ std::floor(NSMidY([infoBarView_ frame]) - (kButtonHeightPx / 2.0)) +
+ kBottomBorderHeightPx;
+ NSRect buttonFrame = NSMakeRect(
+ kButtonLeftMarginPx, buttonY, kButtonWidthPx, kButtonHeightPx);
+ [dropdownButton_ setFrame:buttonFrame];
+ [dropdownButton_ setAutoresizingMask:NSViewMinYMargin | NSViewMaxYMargin];
+ [infoBarView_ addSubview:dropdownButton_];
+
+ // Because the parent view has a bottom border, account for it during
+ // positioning.
+ NSRect extensionFrame = [extensionView_ frame];
+ extensionFrame.origin.y = kBottomBorderHeightPx;
+
+ [extensionView_ setFrame:extensionFrame];
+ // The extension's native view will only have a height that is non-zero if it
+ // already has been loaded and rendered, which is the case when you switch
+ // back to a tab with an extension infobar within it. The reason this is
+ // needed is because the extension view's frame will not have changed in the
+ // above case, so the NSViewFrameDidChangeNotification registered below will
+ // never fire.
+ if (NSHeight(extensionFrame) > 0.0) {
+ NSSize infoBarSize = [[self view] frame].size;
+ infoBarSize.height = [self clampedExtensionViewHeight] +
+ kBottomBorderHeightPx;
+ [[self view] setFrameSize:infoBarSize];
+ [infoBarView_ setFrameSize:infoBarSize];
+ }
+
+ [self adjustExtensionViewSize];
+
+ // These two notification handlers are here to ensure the width of the
+ // native extension view is the same as the browser window's width and that
+ // the parent infobar view matches the height of the extension's native view.
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(extensionViewFrameChanged)
+ name:NSViewFrameDidChangeNotification
+ object:extensionView_];
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(adjustWidthToFitWindow)
+ name:NSWindowDidResizeNotification
+ object:window_];
+}
+
+- (void)extensionViewFrameChanged {
+ [self adjustExtensionViewSize];
+
+ AnimatableView* view = [self animatableView];
+ NSRect infoBarFrame = [view frame];
+ CGFloat newHeight = [self clampedExtensionViewHeight] + kBottomBorderHeightPx;
+ [infoBarView_ setPostsFrameChangedNotifications:NO];
+ infoBarFrame.size.height = newHeight;
+ [infoBarView_ setFrame:infoBarFrame];
+ [infoBarView_ setPostsFrameChangedNotifications:YES];
+ [view animateToNewHeight:newHeight duration:kAnimationDuration];
+}
+
+- (CGFloat)clampedExtensionViewHeight {
+ return std::max(kToolbarMinHeightPx,
+ std::min(NSHeight([extensionView_ frame]), kToolbarMaxHeightPx));
+}
+
+- (void)adjustExtensionViewSize {
+ [extensionView_ setPostsFrameChangedNotifications:NO];
+ NSSize extensionViewSize = [extensionView_ frame].size;
+ extensionViewSize.width = NSWidth([window_ frame]);
+ extensionViewSize.height = [self clampedExtensionViewHeight];
+ [extensionView_ setFrameSize:extensionViewSize];
+ [extensionView_ setPostsFrameChangedNotifications:YES];
+}
+
+- (void)setButtonImage:(NSImage*)image {
+ [dropdownButton_ setImage:image];
+}
+
+@end
+
+InfoBar* ExtensionInfoBarDelegate::CreateInfoBar() {
+ NSWindow* window = [(NSView*)tab_contents_->GetContentNativeView() window];
+ ExtensionInfoBarController* controller =
+ [[ExtensionInfoBarController alloc] initWithDelegate:this
+ window:window];
+ return new InfoBar(controller);
+}
diff --git a/chrome/browser/ui/cocoa/extensions/extension_install_prompt_controller.h b/chrome/browser/ui/cocoa/extensions/extension_install_prompt_controller.h
new file mode 100644
index 0000000..6ae5884
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/extension_install_prompt_controller.h
@@ -0,0 +1,62 @@
+// 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_UI_COCOA_EXTENSION_INSTALL_PROMPT_H_
+#define CHROME_BROWSER_UI_COCOA_EXTENSION_INSTALL_PROMPT_H_
+#pragma once
+
+#include <vector>
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "base/string16.h"
+#include "chrome/browser/extensions/extension_install_ui.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+class Extension;
+class Profile;
+
+// A controller for dialog to let the user install an extension. Created by
+// CrxInstaller.
+@interface ExtensionInstallPromptController : NSWindowController {
+@private
+ IBOutlet NSImageView* iconView_;
+ IBOutlet NSTextField* titleField_;
+ IBOutlet NSTextField* subtitleField_;
+ IBOutlet NSTextField* warningsField_;
+ IBOutlet NSBox* warningsBox_;
+ IBOutlet NSButton* cancelButton_;
+ IBOutlet NSButton* okButton_;
+
+ NSWindow* parentWindow_; // weak
+ Profile* profile_; // weak
+ ExtensionInstallUI::Delegate* delegate_; // weak
+
+ scoped_nsobject<NSString> title_;
+ scoped_nsobject<NSString> warnings_;
+ SkBitmap icon_;
+}
+
+@property (nonatomic, readonly) NSImageView* iconView;
+@property (nonatomic, readonly) NSTextField* titleField;
+@property (nonatomic, readonly) NSTextField* subtitleField;
+@property (nonatomic, readonly) NSTextField* warningsField;
+@property (nonatomic, readonly) NSBox* warningsBox;
+@property (nonatomic, readonly) NSButton* cancelButton;
+@property (nonatomic, readonly) NSButton* okButton;
+
+- (id)initWithParentWindow:(NSWindow*)window
+ profile:(Profile*)profile
+ extension:(const Extension*)extension
+ delegate:(ExtensionInstallUI::Delegate*)delegate
+ icon:(SkBitmap*)bitmap
+ warnings:(const std::vector<string16>&)warnings;
+- (void)runAsModalSheet;
+- (IBAction)cancel:(id)sender;
+- (IBAction)ok:(id)sender;
+
+@end
+
+#endif /* CHROME_BROWSER_UI_COCOA_EXTENSION_INSTALL_PROMPT_H_ */
diff --git a/chrome/browser/ui/cocoa/extensions/extension_install_prompt_controller.mm b/chrome/browser/ui/cocoa/extensions/extension_install_prompt_controller.mm
new file mode 100644
index 0000000..e40fcd4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/extension_install_prompt_controller.mm
@@ -0,0 +1,217 @@
+// 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/ui/cocoa/extensions/extension_install_prompt_controller.h"
+
+#include "app/l10n_util.h"
+#include "app/l10n_util_mac.h"
+#include "base/mac_util.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chrome/common/extensions/extension.h"
+#include "grit/generated_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+
+namespace {
+
+// Maximum height we will adjust controls to when trying to accomodate their
+// contents.
+const CGFloat kMaxControlHeight = 400;
+
+// Adjust a control's height so that its content its not clipped. Returns the
+// amount the control's height had to be adjusted.
+CGFloat AdjustControlHeightToFitContent(NSControl* control) {
+ NSRect currentRect = [control frame];
+ NSRect fitRect = currentRect;
+ fitRect.size.height = kMaxControlHeight;
+ CGFloat desiredHeight = [[control cell] cellSizeForBounds:fitRect].height;
+ CGFloat offset = desiredHeight - currentRect.size.height;
+
+ [control setFrameSize:NSMakeSize(currentRect.size.width,
+ currentRect.size.height + offset)];
+ return offset;
+}
+
+// Moves the control vertically by the specified amount.
+void OffsetControlVertically(NSControl* control, CGFloat amount) {
+ NSPoint origin = [control frame].origin;
+ origin.y += amount;
+ [control setFrameOrigin:origin];
+}
+
+}
+
+@implementation ExtensionInstallPromptController
+
+@synthesize iconView = iconView_;
+@synthesize titleField = titleField_;
+@synthesize subtitleField = subtitleField_;
+@synthesize warningsField = warningsField_;
+@synthesize warningsBox= warningsBox_;
+@synthesize cancelButton = cancelButton_;
+@synthesize okButton = okButton_;
+
+- (id)initWithParentWindow:(NSWindow*)window
+ profile:(Profile*)profile
+ extension:(const Extension*)extension
+ delegate:(ExtensionInstallUI::Delegate*)delegate
+ icon:(SkBitmap*)icon
+ warnings:(const std::vector<string16>&)warnings {
+ NSString* nibpath = nil;
+
+ // We use a different XIB in the case of no warnings, that is a little bit
+ // more nicely laid out.
+ if (warnings.empty()) {
+ nibpath = [mac_util::MainAppBundle()
+ pathForResource:@"ExtensionInstallPromptNoWarnings"
+ ofType:@"nib"];
+ } else {
+ nibpath = [mac_util::MainAppBundle()
+ pathForResource:@"ExtensionInstallPrompt"
+ ofType:@"nib"];
+ }
+
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ parentWindow_ = window;
+ profile_ = profile;
+ icon_ = *icon;
+ delegate_ = delegate;
+
+ title_.reset(
+ [l10n_util::GetNSStringF(IDS_EXTENSION_INSTALL_PROMPT_HEADING,
+ UTF8ToUTF16(extension->name())) retain]);
+
+ // We display the warnings as a simple text string, separated by newlines.
+ if (!warnings.empty()) {
+ string16 joined_warnings;
+ for (size_t i = 0; i < warnings.size(); ++i) {
+ if (i > 0)
+ joined_warnings += UTF8ToUTF16("\n\n");
+
+ joined_warnings += warnings[i];
+ }
+
+ warnings_.reset(
+ [base::SysUTF16ToNSString(joined_warnings) retain]);
+ }
+ }
+ return self;
+}
+
+- (void)runAsModalSheet {
+ [NSApp beginSheet:[self window]
+ modalForWindow:parentWindow_
+ modalDelegate:self
+ didEndSelector:@selector(didEndSheet:returnCode:contextInfo:)
+ contextInfo:nil];
+}
+
+- (IBAction)cancel:(id)sender {
+ delegate_->InstallUIAbort();
+ [NSApp endSheet:[self window]];
+}
+
+- (IBAction)ok:(id)sender {
+ delegate_->InstallUIProceed();
+ [NSApp endSheet:[self window]];
+}
+
+- (void)awakeFromNib {
+ [titleField_ setStringValue:title_.get()];
+
+ NSImage* image = gfx::SkBitmapToNSImage(icon_);
+ [iconView_ setImage:image];
+
+ // Make sure we're the window's delegate as set in the nib.
+ DCHECK_EQ(self, static_cast<ExtensionInstallPromptController*>(
+ [[self window] delegate]));
+
+ // If there are any warnings, then we have to do some special layout.
+ if ([warnings_.get() length] > 0) {
+ [warningsField_ setStringValue:warnings_.get()];
+
+ // The dialog is laid out in the NIB exactly how we want it assuming that
+ // each label fits on one line. However, for each label, we want to allow
+ // wrapping onto multiple lines. So we accumulate an offset by measuring how
+ // big each label wants to be, and comparing it to how bit it actually is.
+ // Then we shift each label down and resize by the appropriate amount, then
+ // finally resize the window.
+ CGFloat totalOffset = 0.0;
+
+ // Text fields.
+ totalOffset += AdjustControlHeightToFitContent(titleField_);
+ OffsetControlVertically(titleField_, -totalOffset);
+
+ totalOffset += AdjustControlHeightToFitContent(subtitleField_);
+ OffsetControlVertically(subtitleField_, -totalOffset);
+
+ CGFloat warningsOffset = AdjustControlHeightToFitContent(warningsField_);
+ OffsetControlVertically(warningsField_, -warningsOffset);
+ totalOffset += warningsOffset;
+
+ NSRect warningsBoxRect = [warningsBox_ frame];
+ warningsBoxRect.origin.y -= totalOffset;
+ warningsBoxRect.size.height += warningsOffset;
+ [warningsBox_ setFrame:warningsBoxRect];
+
+ // buttons are positioned automatically in the XIB.
+
+ // Finally, adjust the window size.
+ NSRect currentRect = [[self window] frame];
+ [[self window] setFrame:NSMakeRect(currentRect.origin.x,
+ currentRect.origin.y - totalOffset,
+ currentRect.size.width,
+ currentRect.size.height + totalOffset)
+ display:NO];
+ }
+}
+
+- (void)didEndSheet:(NSWindow*)sheet
+ returnCode:(int)returnCode
+ contextInfo:(void*)contextInfo {
+ [sheet close];
+}
+
+- (void)windowWillClose:(NSNotification*)notification {
+ [self autorelease];
+}
+
+@end // ExtensionInstallPromptController
+
+
+void ExtensionInstallUI::ShowExtensionInstallUIPrompt2Impl(
+ Profile* profile,
+ Delegate* delegate,
+ const Extension* extension,
+ SkBitmap* icon,
+ const std::vector<string16>& warnings) {
+ Browser* browser = BrowserList::GetLastActiveWithProfile(profile);
+ if (!browser) {
+ delegate->InstallUIAbort();
+ return;
+ }
+
+ BrowserWindow* window = browser->window();
+ if (!window) {
+ delegate->InstallUIAbort();
+ return;
+ }
+
+ gfx::NativeWindow native_window = window->GetNativeHandle();
+
+ ExtensionInstallPromptController* controller =
+ [[ExtensionInstallPromptController alloc]
+ initWithParentWindow:native_window
+ profile:profile
+ extension:extension
+ delegate:delegate
+ icon:icon
+ warnings:warnings];
+
+ [controller runAsModalSheet];
+}
diff --git a/chrome/browser/ui/cocoa/extensions/extension_install_prompt_controller_unittest.mm b/chrome/browser/ui/cocoa/extensions/extension_install_prompt_controller_unittest.mm
new file mode 100644
index 0000000..225aad6
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/extension_install_prompt_controller_unittest.mm
@@ -0,0 +1,286 @@
+// 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/file_path.h"
+#include "base/file_util.h"
+#include "base/path_service.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "base/values.h"
+#import "chrome/browser/extensions/extension_install_ui.h"
+#import "chrome/browser/ui/cocoa/extensions/extension_install_prompt_controller.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/common/chrome_paths.h"
+#include "chrome/common/extensions/extension.h"
+#include "chrome/common/json_value_serializer.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#include "webkit/glue/image_decoder.h"
+
+
+// Base class for our tests.
+class ExtensionInstallPromptControllerTest : public CocoaTest {
+public:
+ ExtensionInstallPromptControllerTest() {
+ PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir_);
+ test_data_dir_ = test_data_dir_.AppendASCII("extensions")
+ .AppendASCII("install_prompt");
+
+ LoadIcon();
+ LoadExtension();
+ }
+
+ protected:
+ void LoadIcon() {
+ std::string file_contents;
+ file_util::ReadFileToString(test_data_dir_.AppendASCII("icon.png"),
+ &file_contents);
+
+ webkit_glue::ImageDecoder decoder;
+ icon_ = decoder.Decode(
+ reinterpret_cast<const unsigned char*>(file_contents.c_str()),
+ file_contents.length());
+ }
+
+ void LoadExtension() {
+ FilePath path = test_data_dir_.AppendASCII("extension.json");
+
+ std::string error;
+ JSONFileValueSerializer serializer(path);
+ scoped_ptr<DictionaryValue> value(static_cast<DictionaryValue*>(
+ serializer.Deserialize(NULL, &error)));
+ if (!value.get()) {
+ LOG(ERROR) << error;
+ return;
+ }
+
+ extension_ = Extension::Create(
+ path.DirName(), Extension::INVALID, *value, false, &error);
+ if (!extension_.get()) {
+ LOG(ERROR) << error;
+ return;
+ }
+ }
+
+ BrowserTestHelper helper_;
+ FilePath test_data_dir_;
+ SkBitmap icon_;
+ scoped_refptr<Extension> extension_;
+};
+
+
+// Mock out the ExtensionInstallUI::Delegate interface so we can ensure the
+// dialog is interacting with it correctly.
+class MockExtensionInstallUIDelegate : public ExtensionInstallUI::Delegate {
+ public:
+ MockExtensionInstallUIDelegate()
+ : proceed_count_(0),
+ abort_count_(0) {}
+
+ // ExtensionInstallUI::Delegate overrides.
+ virtual void InstallUIProceed() {
+ proceed_count_++;
+ }
+
+ virtual void InstallUIAbort() {
+ abort_count_++;
+ }
+
+ int proceed_count() { return proceed_count_; }
+ int abort_count() { return abort_count_; }
+
+ protected:
+ int proceed_count_;
+ int abort_count_;
+};
+
+// Test that we can load the two kinds of prompts correctly, that the outlets
+// are hooked up, and that the dialog calls cancel when cancel is pressed.
+TEST_F(ExtensionInstallPromptControllerTest, BasicsNormalCancel) {
+ scoped_ptr<MockExtensionInstallUIDelegate> delegate(
+ new MockExtensionInstallUIDelegate);
+
+ std::vector<string16> warnings;
+ warnings.push_back(UTF8ToUTF16("warning 1"));
+
+ scoped_nsobject<ExtensionInstallPromptController>
+ controller([[ExtensionInstallPromptController alloc]
+ initWithParentWindow:test_window()
+ profile:helper_.profile()
+ extension:extension_.get()
+ delegate:delegate.get()
+ icon:&icon_
+ warnings:warnings]);
+
+ [controller window]; // force nib load
+
+ // Test the right nib loaded.
+ EXPECT_NSEQ(@"ExtensionInstallPrompt", [controller windowNibName]);
+
+ // Check all the controls.
+ // Make sure everything is non-nil, and that the fields that are
+ // auto-translated don't start with a caret (that would indicate that they
+ // were not translated).
+ EXPECT_TRUE([controller iconView] != nil);
+ EXPECT_TRUE([[controller iconView] image] != nil);
+
+ EXPECT_TRUE([controller titleField] != nil);
+ EXPECT_NE(0u, [[[controller titleField] stringValue] length]);
+
+ EXPECT_TRUE([controller subtitleField] != nil);
+ EXPECT_NE(0u, [[[controller subtitleField] stringValue] length]);
+ EXPECT_NE('^', [[[controller subtitleField] stringValue] characterAtIndex:0]);
+
+ EXPECT_TRUE([controller warningsField] != nil);
+ EXPECT_NSEQ([[controller warningsField] stringValue],
+ base::SysUTF16ToNSString(warnings[0]));
+
+ EXPECT_TRUE([controller warningsBox] != nil);
+
+ EXPECT_TRUE([controller cancelButton] != nil);
+ EXPECT_NE(0u, [[[controller cancelButton] stringValue] length]);
+ EXPECT_NE('^', [[[controller cancelButton] stringValue] characterAtIndex:0]);
+
+ EXPECT_TRUE([controller okButton] != nil);
+ EXPECT_NE(0u, [[[controller okButton] stringValue] length]);
+ EXPECT_NE('^', [[[controller okButton] stringValue] characterAtIndex:0]);
+
+ // Test that cancel calls our delegate.
+ [controller cancel:nil];
+ EXPECT_EQ(1, delegate->abort_count());
+ EXPECT_EQ(0, delegate->proceed_count());
+}
+
+
+TEST_F(ExtensionInstallPromptControllerTest, BasicsNormalOK) {
+ scoped_ptr<MockExtensionInstallUIDelegate> delegate(
+ new MockExtensionInstallUIDelegate);
+
+ std::vector<string16> warnings;
+ warnings.push_back(UTF8ToUTF16("warning 1"));
+
+ scoped_nsobject<ExtensionInstallPromptController>
+ controller([[ExtensionInstallPromptController alloc]
+ initWithParentWindow:test_window()
+ profile:helper_.profile()
+ extension:extension_.get()
+ delegate:delegate.get()
+ icon:&icon_
+ warnings:warnings]);
+
+ [controller window]; // force nib load
+ [controller ok:nil];
+
+ EXPECT_EQ(0, delegate->abort_count());
+ EXPECT_EQ(1, delegate->proceed_count());
+}
+
+// Test that controls get repositioned when there are two warnings vs one
+// warning.
+TEST_F(ExtensionInstallPromptControllerTest, MultipleWarnings) {
+ scoped_ptr<MockExtensionInstallUIDelegate> delegate1(
+ new MockExtensionInstallUIDelegate);
+ scoped_ptr<MockExtensionInstallUIDelegate> delegate2(
+ new MockExtensionInstallUIDelegate);
+
+ std::vector<string16> one_warning;
+ one_warning.push_back(UTF8ToUTF16("warning 1"));
+
+ std::vector<string16> two_warnings;
+ two_warnings.push_back(UTF8ToUTF16("warning 1"));
+ two_warnings.push_back(UTF8ToUTF16("warning 2"));
+
+ scoped_nsobject<ExtensionInstallPromptController>
+ controller1([[ExtensionInstallPromptController alloc]
+ initWithParentWindow:test_window()
+ profile:helper_.profile()
+ extension:extension_.get()
+ delegate:delegate1.get()
+ icon:&icon_
+ warnings:one_warning]);
+
+ [controller1 window]; // force nib load
+
+ scoped_nsobject<ExtensionInstallPromptController>
+ controller2([[ExtensionInstallPromptController alloc]
+ initWithParentWindow:test_window()
+ profile:helper_.profile()
+ extension:extension_.get()
+ delegate:delegate2.get()
+ icon:&icon_
+ warnings:two_warnings]);
+
+ [controller2 window]; // force nib load
+
+ // Test control positioning. We don't test exact positioning because we don't
+ // want this to depend on string details and localization. But we do know the
+ // relative effect that adding a second warning should have on the layout.
+ ASSERT_LT([[controller1 window] frame].size.height,
+ [[controller2 window] frame].size.height);
+
+ ASSERT_LT([[controller1 warningsField] frame].size.height,
+ [[controller2 warningsField] frame].size.height);
+
+ ASSERT_LT([[controller1 warningsBox] frame].size.height,
+ [[controller2 warningsBox] frame].size.height);
+
+ ASSERT_EQ([[controller1 warningsBox] frame].origin.y,
+ [[controller2 warningsBox] frame].origin.y);
+
+ ASSERT_LT([[controller1 subtitleField] frame].origin.y,
+ [[controller2 subtitleField] frame].origin.y);
+
+ ASSERT_LT([[controller1 titleField] frame].origin.y,
+ [[controller2 titleField] frame].origin.y);
+}
+
+// Test that we can load the skinny prompt correctly, and that the outlets are
+// are hooked up.
+TEST_F(ExtensionInstallPromptControllerTest, BasicsSkinny) {
+ scoped_ptr<MockExtensionInstallUIDelegate> delegate(
+ new MockExtensionInstallUIDelegate);
+
+ // No warnings should trigger skinny prompt.
+ std::vector<string16> warnings;
+
+ scoped_nsobject<ExtensionInstallPromptController>
+ controller([[ExtensionInstallPromptController alloc]
+ initWithParentWindow:test_window()
+ profile:helper_.profile()
+ extension:extension_.get()
+ delegate:delegate.get()
+ icon:&icon_
+ warnings:warnings]);
+
+ [controller window]; // force nib load
+
+ // Test the right nib loaded.
+ EXPECT_NSEQ(@"ExtensionInstallPromptNoWarnings", [controller windowNibName]);
+
+ // Check all the controls.
+ // In the skinny prompt, only the icon, title and buttons are non-nill.
+ // Everything else is nil.
+ EXPECT_TRUE([controller iconView] != nil);
+ EXPECT_TRUE([[controller iconView] image] != nil);
+
+ EXPECT_TRUE([controller titleField] != nil);
+ EXPECT_NE(0u, [[[controller titleField] stringValue] length]);
+
+ EXPECT_TRUE([controller cancelButton] != nil);
+ EXPECT_NE(0u, [[[controller cancelButton] stringValue] length]);
+ EXPECT_NE('^', [[[controller cancelButton] stringValue] characterAtIndex:0]);
+
+ EXPECT_TRUE([controller okButton] != nil);
+ EXPECT_NE(0u, [[[controller okButton] stringValue] length]);
+ EXPECT_NE('^', [[[controller okButton] stringValue] characterAtIndex:0]);
+
+ EXPECT_TRUE([controller subtitleField] == nil);
+ EXPECT_TRUE([controller warningsField] == nil);
+ EXPECT_TRUE([controller warningsBox] == nil);
+}
diff --git a/chrome/browser/ui/cocoa/extensions/extension_popup_controller.h b/chrome/browser/ui/cocoa/extensions/extension_popup_controller.h
new file mode 100644
index 0000000..91bdba6
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/extension_popup_controller.h
@@ -0,0 +1,99 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_EXTENSIONS_EXTENSION_POPUP_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_EXTENSIONS_EXTENSION_POPUP_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/cocoa_protocols_mac.h"
+#import "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/ui/cocoa/info_bubble_view.h"
+#include "googleurl/src/gurl.h"
+
+
+class Browser;
+class DevtoolsNotificationBridge;
+class ExtensionHost;
+@class InfoBubbleWindow;
+class NotificationRegistrar;
+
+// This controller manages a single browser action popup that can appear once a
+// user has clicked on a browser action button. It instantiates the extension
+// popup view showing the content and resizes the window to accomodate any size
+// changes as they occur.
+//
+// There can only be one browser action popup open at a time, so a static
+// variable holds a reference to the current popup.
+@interface ExtensionPopupController : NSWindowController<NSWindowDelegate> {
+ @private
+ // The native extension view retrieved from the extension host. Weak.
+ NSView* extensionView_;
+
+ // The popup's parent window. Weak.
+ NSWindow* parentWindow_;
+
+ // Where the window is anchored. Right now it's the bottom center of the
+ // browser action button.
+ NSPoint anchor_;
+
+ // The current frame of the extension view. Cached to prevent setting the
+ // frame if the size hasn't changed.
+ NSRect extensionFrame_;
+
+ // The extension host object.
+ scoped_ptr<ExtensionHost> host_;
+
+ scoped_ptr<NotificationRegistrar> registrar_;
+ scoped_ptr<DevtoolsNotificationBridge> notificationBridge_;
+
+ // Whether the popup has a devtools window attached to it.
+ BOOL beingInspected_;
+}
+
+// Returns the ExtensionHost object associated with this popup.
+- (ExtensionHost*)extensionHost;
+
+// Starts the process of showing the given popup URL. Instantiates an
+// ExtensionPopupController with the parent window retrieved from |browser|, a
+// host for the popup created by the extension process manager specific to the
+// browser profile and the remaining arguments |anchoredAt| and |arrowLocation|.
+// |anchoredAt| is expected to be in the window's coordinates at the bottom
+// center of the browser action button.
+// The actual display of the popup is delayed until the page contents finish
+// loading in order to minimize UI flashing and resizing.
+// Passing YES to |devMode| will launch the webkit inspector for the popup,
+// and prevent the popup from closing when focus is lost. It will be closed
+// after the inspector is closed, or another popup is opened.
++ (ExtensionPopupController*)showURL:(GURL)url
+ inBrowser:(Browser*)browser
+ anchoredAt:(NSPoint)anchoredAt
+ arrowLocation:(info_bubble::BubbleArrowLocation)
+ arrowLocation
+ devMode:(BOOL)devMode;
+
+// Returns the controller used to display the popup being shown. If no popup is
+// currently open, then nil is returned. Static because only one extension popup
+// window can be open at a time.
++ (ExtensionPopupController*)popup;
+
+// Whether the popup is in the process of closing (via Core Animation).
+- (BOOL)isClosing;
+
+// Show the dev tools attached to the popup.
+- (void)showDevTools;
+@end
+
+@interface ExtensionPopupController(TestingAPI)
+// Returns a weak pointer to the current popup's view.
+- (NSView*)view;
+// Returns the minimum allowed size for an extension popup.
++ (NSSize)minPopupSize;
+// Returns the maximum allowed size for an extension popup.
++ (NSSize)maxPopupSize;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_EXTENSIONS_EXTENSION_POPUP_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/extensions/extension_popup_controller.mm b/chrome/browser/ui/cocoa/extensions/extension_popup_controller.mm
new file mode 100644
index 0000000..bf3408e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/extension_popup_controller.mm
@@ -0,0 +1,338 @@
+// 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/ui/cocoa/extensions/extension_popup_controller.h"
+
+#include <algorithm>
+
+#include "chrome/browser/debugger/devtools_manager.h"
+#include "chrome/browser/extensions/extension_host.h"
+#include "chrome/browser/extensions/extension_process_manager.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/ui/browser.h"
+#import "chrome/browser/ui/cocoa/browser_window_cocoa.h"
+#import "chrome/browser/ui/cocoa/extension_view_mac.h"
+#import "chrome/browser/ui/cocoa/info_bubble_window.h"
+#include "chrome/common/notification_registrar.h"
+#include "chrome/common/notification_service.h"
+
+namespace {
+// The duration for any animations that might be invoked by this controller.
+const NSTimeInterval kAnimationDuration = 0.2;
+
+// There should only be one extension popup showing at one time. Keep a
+// reference to it here.
+static ExtensionPopupController* gPopup;
+
+// Given a value and a rage, clamp the value into the range.
+CGFloat Clamp(CGFloat value, CGFloat min, CGFloat max) {
+ return std::max(min, std::min(max, value));
+}
+
+} // namespace
+
+class DevtoolsNotificationBridge : public NotificationObserver {
+ public:
+ explicit DevtoolsNotificationBridge(ExtensionPopupController* controller)
+ : controller_(controller) {}
+
+ void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ switch (type.value) {
+ case NotificationType::EXTENSION_HOST_DID_STOP_LOADING: {
+ if (Details<ExtensionHost>([controller_ extensionHost]) == details)
+ [controller_ showDevTools];
+ break;
+ }
+ case NotificationType::DEVTOOLS_WINDOW_CLOSING: {
+ RenderViewHost* rvh = [controller_ extensionHost]->render_view_host();
+ if (Details<RenderViewHost>(rvh) == details)
+ // Allow the devtools to finish detaching before we close the popup
+ [controller_ performSelector:@selector(close)
+ withObject:nil
+ afterDelay:0.0];
+ break;
+ }
+ default: {
+ NOTREACHED() << "Received unexpected notification";
+ break;
+ }
+ };
+ }
+
+ private:
+ ExtensionPopupController* controller_;
+};
+
+@interface ExtensionPopupController(Private)
+// Callers should be using the public static method for initialization.
+// NOTE: This takes ownership of |host|.
+- (id)initWithHost:(ExtensionHost*)host
+ parentWindow:(NSWindow*)parentWindow
+ anchoredAt:(NSPoint)anchoredAt
+ arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
+ devMode:(BOOL)devMode;
+
+// Called when the extension's hosted NSView has been resized.
+- (void)extensionViewFrameChanged;
+@end
+
+@implementation ExtensionPopupController
+
+- (id)initWithHost:(ExtensionHost*)host
+ parentWindow:(NSWindow*)parentWindow
+ anchoredAt:(NSPoint)anchoredAt
+ arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
+ devMode:(BOOL)devMode {
+
+ parentWindow_ = parentWindow;
+ anchor_ = [parentWindow convertBaseToScreen:anchoredAt];
+ host_.reset(host);
+ beingInspected_ = devMode;
+
+ scoped_nsobject<InfoBubbleView> view([[InfoBubbleView alloc] init]);
+ if (!view.get())
+ return nil;
+ [view setArrowLocation:arrowLocation];
+ [view setBubbleType:info_bubble::kWhiteInfoBubble];
+
+ host->view()->set_is_toolstrip(NO);
+
+ extensionView_ = host->view()->native_view();
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
+ selector:@selector(extensionViewFrameChanged)
+ name:NSViewFrameDidChangeNotification
+ object:extensionView_];
+
+ // Watch to see if the parent window closes, and if so, close this one.
+ [center addObserver:self
+ selector:@selector(parentWindowWillClose:)
+ name:NSWindowWillCloseNotification
+ object:parentWindow_];
+
+ [view addSubview:extensionView_];
+ scoped_nsobject<InfoBubbleWindow> window(
+ [[InfoBubbleWindow alloc]
+ initWithContentRect:NSZeroRect
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:YES]);
+ if (!window.get())
+ return nil;
+
+ [window setDelegate:self];
+ [window setContentView:view];
+ self = [super initWithWindow:window];
+ if (beingInspected_) {
+ // Listen for the the devtools window closing.
+ notificationBridge_.reset(new DevtoolsNotificationBridge(self));
+ registrar_.reset(new NotificationRegistrar);
+ registrar_->Add(notificationBridge_.get(),
+ NotificationType::DEVTOOLS_WINDOW_CLOSING,
+ Source<Profile>(host->profile()));
+ registrar_->Add(notificationBridge_.get(),
+ NotificationType::EXTENSION_HOST_DID_STOP_LOADING,
+ Source<Profile>(host->profile()));
+ }
+ return self;
+}
+
+- (void)showDevTools {
+ DevToolsManager::GetInstance()->OpenDevToolsWindow(host_->render_view_host());
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+- (void)parentWindowWillClose:(NSNotification*)notification {
+ [self close];
+}
+
+- (void)windowWillClose:(NSNotification *)notification {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [gPopup autorelease];
+ gPopup = nil;
+}
+
+- (void)windowDidResignKey:(NSNotification *)notification {
+ NSWindow* window = [self window];
+ DCHECK_EQ([notification object], window);
+ // If the window isn't visible, it is already closed, and this notification
+ // has been sent as part of the closing operation, so no need to close.
+ if ([window isVisible] && !beingInspected_) {
+ [self close];
+ }
+}
+
+- (void)close {
+ [parentWindow_ removeChildWindow:[self window]];
+
+ // No longer have a parent window, so nil out the pointer and deregister for
+ // notifications.
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center removeObserver:self
+ name:NSWindowWillCloseNotification
+ object:parentWindow_];
+ parentWindow_ = nil;
+ [super close];
+}
+
+- (BOOL)isClosing {
+ return [static_cast<InfoBubbleWindow*>([self window]) isClosing];
+}
+
+- (ExtensionHost*)extensionHost {
+ return host_.get();
+}
+
++ (ExtensionPopupController*)showURL:(GURL)url
+ inBrowser:(Browser*)browser
+ anchoredAt:(NSPoint)anchoredAt
+ arrowLocation:(info_bubble::BubbleArrowLocation)
+ arrowLocation
+ devMode:(BOOL)devMode {
+ DCHECK([NSThread isMainThread]);
+ DCHECK(browser);
+ if (!browser)
+ return nil;
+
+ ExtensionProcessManager* manager =
+ browser->profile()->GetExtensionProcessManager();
+ DCHECK(manager);
+ if (!manager)
+ return nil;
+
+ ExtensionHost* host = manager->CreatePopup(url, browser);
+ DCHECK(host);
+ if (!host)
+ return nil;
+
+ // Make absolutely sure that no popups are leaked.
+ if (gPopup) {
+ if ([[gPopup window] isVisible])
+ [gPopup close];
+
+ [gPopup autorelease];
+ gPopup = nil;
+ }
+ DCHECK(!gPopup);
+
+ // Takes ownership of |host|. Also will autorelease itself when the popup is
+ // closed, so no need to do that here.
+ gPopup = [[ExtensionPopupController alloc]
+ initWithHost:host
+ parentWindow:browser->window()->GetNativeHandle()
+ anchoredAt:anchoredAt
+ arrowLocation:arrowLocation
+ devMode:devMode];
+ return gPopup;
+}
+
++ (ExtensionPopupController*)popup {
+ return gPopup;
+}
+
+- (void)extensionViewFrameChanged {
+ // If there are no changes in the width or height of the frame, then ignore.
+ if (NSEqualSizes([extensionView_ frame].size, extensionFrame_.size))
+ return;
+
+ extensionFrame_ = [extensionView_ frame];
+ // Constrain the size of the view.
+ [extensionView_ setFrameSize:NSMakeSize(
+ Clamp(NSWidth(extensionFrame_),
+ ExtensionViewMac::kMinWidth,
+ ExtensionViewMac::kMaxWidth),
+ Clamp(NSHeight(extensionFrame_),
+ ExtensionViewMac::kMinHeight,
+ ExtensionViewMac::kMaxHeight))];
+
+ // Pad the window by half of the rounded corner radius to prevent the
+ // extension's view from bleeding out over the corners.
+ CGFloat inset = info_bubble::kBubbleCornerRadius / 2.0;
+ [extensionView_ setFrameOrigin:NSMakePoint(inset, inset)];
+
+ NSRect frame = [extensionView_ frame];
+ frame.size.height += info_bubble::kBubbleArrowHeight +
+ info_bubble::kBubbleCornerRadius;
+ frame.size.width += info_bubble::kBubbleCornerRadius;
+ frame = [extensionView_ convertRectToBase:frame];
+ // Adjust the origin according to the height and width so that the arrow is
+ // positioned correctly at the middle and slightly down from the button.
+ NSPoint windowOrigin = anchor_;
+ NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
+ info_bubble::kBubbleArrowWidth / 2.0,
+ info_bubble::kBubbleArrowHeight / 2.0);
+ offsets = [extensionView_ convertSize:offsets toView:nil];
+ windowOrigin.x -= NSWidth(frame) - offsets.width;
+ windowOrigin.y -= NSHeight(frame) - offsets.height;
+ frame.origin = windowOrigin;
+
+ // Is the window still animating in? If so, then cancel that and create a new
+ // animation setting the opacity and new frame value. Otherwise the current
+ // animation will continue after this frame is set, reverting the frame to
+ // what it was when the animation started.
+ NSWindow* window = [self window];
+ if ([window isVisible] && [[window animator] alphaValue] < 1.0) {
+ [NSAnimationContext beginGrouping];
+ [[NSAnimationContext currentContext] setDuration:kAnimationDuration];
+ [[window animator] setAlphaValue:1.0];
+ [[window animator] setFrame:frame display:YES];
+ [NSAnimationContext endGrouping];
+ } else {
+ [window setFrame:frame display:YES];
+ }
+
+ // A NSViewFrameDidChangeNotification won't be sent until the extension view
+ // content is loaded. The window is hidden on init, so show it the first time
+ // the notification is fired (and consequently the view contents have loaded).
+ if (![window isVisible]) {
+ [self showWindow:self];
+ }
+}
+
+// We want this to be a child of a browser window. addChildWindow: (called from
+// this function) will bring the window on-screen; unfortunately,
+// [NSWindowController showWindow:] will also bring it on-screen (but will cause
+// unexpected changes to the window's position). We cannot have an
+// addChildWindow: and a subsequent showWindow:. Thus, we have our own version.
+- (void)showWindow:(id)sender {
+ [parentWindow_ addChildWindow:[self window] ordered:NSWindowAbove];
+ [[self window] makeKeyAndOrderFront:self];
+}
+
+- (void)windowDidResize:(NSNotification*)notification {
+ // Let the extension view know, so that it can tell plugins.
+ if (host_->view())
+ host_->view()->WindowFrameChanged();
+}
+
+- (void)windowDidMove:(NSNotification*)notification {
+ // Let the extension view know, so that it can tell plugins.
+ if (host_->view())
+ host_->view()->WindowFrameChanged();
+}
+
+// Private (TestingAPI)
+- (NSView*)view {
+ return extensionView_;
+}
+
+// Private (TestingAPI)
++ (NSSize)minPopupSize {
+ NSSize minSize = {ExtensionViewMac::kMinWidth, ExtensionViewMac::kMinHeight};
+ return minSize;
+}
+
+// Private (TestingAPI)
++ (NSSize)maxPopupSize {
+ NSSize maxSize = {ExtensionViewMac::kMaxWidth, ExtensionViewMac::kMaxHeight};
+ return maxSize;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/extensions/extension_popup_controller_unittest.mm b/chrome/browser/ui/cocoa/extensions/extension_popup_controller_unittest.mm
new file mode 100644
index 0000000..0e74e5e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/extensions/extension_popup_controller_unittest.mm
@@ -0,0 +1,89 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/extensions/extension_process_manager.h"
+#include "chrome/browser/extensions/extensions_service.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
+#include "chrome/test/testing_profile.h"
+
+namespace {
+
+class ExtensionTestingProfile : public TestingProfile {
+ public:
+ ExtensionTestingProfile() {}
+
+ FilePath GetExtensionsInstallDir() {
+ return GetPath().AppendASCII(ExtensionsService::kInstallDirectoryName);
+ }
+
+ void InitExtensionProfile() {
+ DCHECK(!GetExtensionProcessManager());
+ DCHECK(!GetExtensionsService());
+
+ manager_.reset(ExtensionProcessManager::Create(this));
+ service_ = new ExtensionsService(this,
+ CommandLine::ForCurrentProcess(),
+ GetExtensionsInstallDir(),
+ false);
+ service_->set_extensions_enabled(true);
+ service_->set_show_extensions_prompts(false);
+ service_->ClearProvidersForTesting();
+ service_->Init();
+ }
+
+ void ShutdownExtensionProfile() {
+ manager_.reset();
+ service_ = NULL;
+ }
+
+ virtual ExtensionProcessManager* GetExtensionProcessManager() {
+ return manager_.get();
+ }
+
+ virtual ExtensionsService* GetExtensionsService() {
+ return service_.get();
+ }
+
+ private:
+ scoped_ptr<ExtensionProcessManager> manager_;
+ scoped_refptr<ExtensionsService> service_;
+
+ DISALLOW_COPY_AND_ASSIGN(ExtensionTestingProfile);
+};
+
+class ExtensionPopupControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ profile_.reset(new ExtensionTestingProfile());
+ profile_->InitExtensionProfile();
+ browser_.reset(new Browser(Browser::TYPE_NORMAL, profile_.get()));
+ [ExtensionPopupController showURL:GURL("http://google.com")
+ inBrowser:browser_.get()
+ anchoredAt:NSZeroPoint
+ arrowLocation:info_bubble::kTopRight
+ devMode:NO];
+ }
+ virtual void TearDown() {
+ profile_->ShutdownExtensionProfile();
+ [[ExtensionPopupController popup] close];
+ CocoaTest::TearDown();
+ }
+
+ protected:
+ scoped_ptr<Browser> browser_;
+ scoped_ptr<ExtensionTestingProfile> profile_;
+};
+
+TEST_F(ExtensionPopupControllerTest, DISABLED_Basics) {
+ // TODO(andybons): Better mechanisms for mocking out the extensions service
+ // and extensions for easy testing need to be implemented.
+ // http://crbug.com/28316
+ EXPECT_TRUE([ExtensionPopupController popup]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/external_protocol_dialog.h b/chrome/browser/ui/cocoa/external_protocol_dialog.h
new file mode 100644
index 0000000..224c280
--- /dev/null
+++ b/chrome/browser/ui/cocoa/external_protocol_dialog.h
@@ -0,0 +1,19 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/time.h"
+#include "googleurl/src/gurl.h"
+
+@interface ExternalProtocolDialogController : NSObject {
+ @private
+ NSAlert* alert_;
+ GURL url_;
+ base::Time creation_time_;
+};
+
+- (id)initWithGURL:(const GURL*)url;
+
+@end
diff --git a/chrome/browser/ui/cocoa/external_protocol_dialog.mm b/chrome/browser/ui/cocoa/external_protocol_dialog.mm
new file mode 100644
index 0000000..8dafab9
--- /dev/null
+++ b/chrome/browser/ui/cocoa/external_protocol_dialog.mm
@@ -0,0 +1,152 @@
+// 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/ui/cocoa/external_protocol_dialog.h"
+
+#include "app/l10n_util_mac.h"
+#include "base/message_loop.h"
+#include "base/metrics/histogram.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/external_protocol_handler.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+
+///////////////////////////////////////////////////////////////////////////////
+// ExternalProtocolHandler
+
+// static
+void ExternalProtocolHandler::RunExternalProtocolDialog(
+ const GURL& url, int render_process_host_id, int routing_id) {
+ [[ExternalProtocolDialogController alloc] initWithGURL:&url];
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// ExternalProtocolDialogController
+
+@interface ExternalProtocolDialogController(Private)
+- (void)alertEnded:(NSAlert *)alert
+ returnCode:(int)returnCode
+ contextInfo:(void*)contextInfo;
+- (string16)appNameForProtocol;
+@end
+
+@implementation ExternalProtocolDialogController
+- (id)initWithGURL:(const GURL*)url {
+ DCHECK_EQ(MessageLoop::TYPE_UI, MessageLoop::current()->type());
+
+ url_ = *url;
+ creation_time_ = base::Time::Now();
+
+ string16 appName = [self appNameForProtocol];
+ if (appName.length() == 0) {
+ // No registered apps for this protocol; give up and go home.
+ [self autorelease];
+ return nil;
+ }
+
+ alert_ = [[NSAlert alloc] init];
+
+ [alert_ setMessageText:
+ l10n_util::GetNSStringWithFixup(IDS_EXTERNAL_PROTOCOL_TITLE)];
+
+ NSButton* allowButton = [alert_ addButtonWithTitle:
+ l10n_util::GetNSStringWithFixup(IDS_EXTERNAL_PROTOCOL_OK_BUTTON_TEXT)];
+ [allowButton setKeyEquivalent:@""]; // disallow as default
+ [alert_ addButtonWithTitle:
+ l10n_util::GetNSStringWithFixup(
+ IDS_EXTERNAL_PROTOCOL_CANCEL_BUTTON_TEXT)];
+
+ const int kMaxUrlWithoutSchemeSize = 256;
+ std::wstring elided_url_without_scheme;
+ ElideString(ASCIIToWide(url_.possibly_invalid_spec()),
+ kMaxUrlWithoutSchemeSize, &elided_url_without_scheme);
+
+ NSString* urlString = l10n_util::GetNSStringFWithFixup(
+ IDS_EXTERNAL_PROTOCOL_INFORMATION,
+ ASCIIToUTF16(url_.scheme() + ":"),
+ WideToUTF16(elided_url_without_scheme));
+ NSString* appString = l10n_util::GetNSStringFWithFixup(
+ IDS_EXTERNAL_PROTOCOL_APPLICATION_TO_LAUNCH,
+ appName);
+ NSString* warningString =
+ l10n_util::GetNSStringWithFixup(IDS_EXTERNAL_PROTOCOL_WARNING);
+ NSString* informativeText =
+ [NSString stringWithFormat:@"%@\n\n%@\n\n%@",
+ urlString,
+ appString,
+ warningString];
+
+ [alert_ setInformativeText:informativeText];
+
+ [alert_ setShowsSuppressionButton:YES];
+ [[alert_ suppressionButton] setTitle:
+ l10n_util::GetNSStringWithFixup(IDS_EXTERNAL_PROTOCOL_CHECKBOX_TEXT)];
+
+ [alert_ beginSheetModalForWindow:nil // nil here makes it app-modal
+ modalDelegate:self
+ didEndSelector:@selector(alertEnded:returnCode:contextInfo:)
+ contextInfo:nil];
+
+ return self;
+}
+
+- (void)dealloc {
+ [alert_ release];
+
+ [super dealloc];
+}
+
+- (void)alertEnded:(NSAlert *)alert
+ returnCode:(int)returnCode
+ contextInfo:(void*)contextInfo {
+ ExternalProtocolHandler::BlockState blockState =
+ ExternalProtocolHandler::UNKNOWN;
+ switch (returnCode) {
+ case NSAlertFirstButtonReturn:
+ blockState = ExternalProtocolHandler::DONT_BLOCK;
+ break;
+ case NSAlertSecondButtonReturn:
+ blockState = ExternalProtocolHandler::BLOCK;
+ break;
+ default:
+ NOTREACHED();
+ }
+
+ // Set the "don't warn me again" info.
+ if ([[alert_ suppressionButton] state] == NSOnState)
+ ExternalProtocolHandler::SetBlockState(url_.scheme(), blockState);
+
+ if (blockState == ExternalProtocolHandler::DONT_BLOCK) {
+ UMA_HISTOGRAM_LONG_TIMES("clickjacking.launch_url",
+ base::Time::Now() - creation_time_);
+
+ ExternalProtocolHandler::LaunchUrlWithoutSecurityCheck(url_);
+ }
+
+ [self autorelease];
+}
+
+- (string16)appNameForProtocol {
+ NSURL* url = [NSURL URLWithString:
+ base::SysUTF8ToNSString(url_.possibly_invalid_spec())];
+ CFURLRef openingApp = NULL;
+ OSStatus status = LSGetApplicationForURL((CFURLRef)url,
+ kLSRolesAll,
+ NULL,
+ &openingApp);
+ if (status != noErr) {
+ // likely kLSApplicationNotFoundErr
+ return string16();
+ }
+ NSString* appPath = [(NSURL*)openingApp path];
+ CFRelease(openingApp); // NOT A BUG; LSGetApplicationForURL retains for us
+ NSString* appDisplayName =
+ [[NSFileManager defaultManager] displayNameAtPath:appPath];
+
+ return base::SysNSStringToUTF16(appDisplayName);
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/fast_resize_view.h b/chrome/browser/ui/cocoa/fast_resize_view.h
new file mode 100644
index 0000000..1da6004
--- /dev/null
+++ b/chrome/browser/ui/cocoa/fast_resize_view.h
@@ -0,0 +1,29 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_FAST_RESIZE_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_FAST_RESIZE_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// A Cocoa view that supports an alternate resizing mode, normally used when
+// animations are in progress. In normal resizing mode, subviews are sized to
+// completely fill this view's bounds. In fast resizing mode, the subviews'
+// size is not changed and the subview is clipped to fit, if necessary. Fast
+// resize mode is useful when animating a view that normally takes a significant
+// amount of time to relayout and redraw when its size is changed.
+@interface FastResizeView : NSView {
+ @private
+ BOOL fastResizeMode_;
+}
+
+// Turns fast resizing mode on or off, which determines how this view resizes
+// its subviews. Turning fast resizing mode off has the effect of immediately
+// resizing subviews to fit; callers do not need to explictly call |setFrame:|
+// to trigger a resize.
+- (void)setFastResizeMode:(BOOL)fastResizeMode;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_FAST_RESIZE_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/fast_resize_view.mm b/chrome/browser/ui/cocoa/fast_resize_view.mm
new file mode 100644
index 0000000..8755bc4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/fast_resize_view.mm
@@ -0,0 +1,65 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+#import "chrome/browser/ui/cocoa/fast_resize_view.h"
+
+#include "base/logging.h"
+
+@interface FastResizeView (PrivateMethods)
+// Lays out this views subviews. If fast resize mode is on, does not resize any
+// subviews and instead pegs them to the top left. If fast resize mode is off,
+// sets the subviews' frame to be equal to this view's bounds.
+- (void)layoutSubviews;
+@end
+
+@implementation FastResizeView
+- (void)setFastResizeMode:(BOOL)fastResizeMode {
+ fastResizeMode_ = fastResizeMode;
+
+ // Force a relayout when coming out of fast resize mode.
+ if (!fastResizeMode_)
+ [self layoutSubviews];
+}
+
+- (void)resizeSubviewsWithOldSize:(NSSize)oldSize {
+ [self layoutSubviews];
+}
+
+- (void)drawRect:(NSRect)dirtyRect {
+ // If we are in fast resize mode, our subviews may not completely cover our
+ // bounds, so we fill with white. If we are not in fast resize mode, we do
+ // not need to draw anything.
+ if (fastResizeMode_) {
+ [[NSColor whiteColor] set];
+ NSRectFill(dirtyRect);
+ }
+}
+
+
+@end
+
+@implementation FastResizeView (PrivateMethods)
+- (void)layoutSubviews {
+ // There should never be more than one subview. There can be zero, if we are
+ // in the process of switching tabs or closing the window. In those cases, no
+ // layout is needed.
+ NSArray* subviews = [self subviews];
+ DCHECK([subviews count] <= 1);
+ if ([subviews count] < 1)
+ return;
+
+ NSView* subview = [subviews objectAtIndex:0];
+ NSRect bounds = [self bounds];
+
+ if (fastResizeMode_) {
+ NSRect frame = [subview frame];
+ frame.origin.x = 0;
+ frame.origin.y = NSHeight(bounds) - NSHeight(frame);
+ [subview setFrame:frame];
+ } else {
+ [subview setFrame:bounds];
+ }
+}
+@end
diff --git a/chrome/browser/ui/cocoa/fast_resize_view_unittest.mm b/chrome/browser/ui/cocoa/fast_resize_view_unittest.mm
new file mode 100644
index 0000000..d64be3f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/fast_resize_view_unittest.mm
@@ -0,0 +1,60 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/fast_resize_view.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+
+namespace {
+
+class FastResizeViewTest : public CocoaTest {
+ public:
+ FastResizeViewTest() {
+ NSRect frame = NSMakeRect(0, 0, 100, 30);
+ scoped_nsobject<FastResizeView> view(
+ [[FastResizeView alloc] initWithFrame:frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+
+ scoped_nsobject<NSView> childView([[NSView alloc] initWithFrame:frame]);
+ childView_ = childView.get();
+ [view_ addSubview:childView_];
+ }
+
+ FastResizeView* view_;
+ NSView* childView_;
+};
+
+TEST_VIEW(FastResizeViewTest, view_);
+
+TEST_F(FastResizeViewTest, TestResizingOfChildren) {
+ NSRect squareFrame = NSMakeRect(0, 0, 200, 200);
+ NSRect rectFrame = NSMakeRect(1, 1, 150, 300);
+
+ // Test that changing the view's frame also changes the child's frame.
+ [view_ setFrame:squareFrame];
+ EXPECT_TRUE(NSEqualRects([view_ bounds], [childView_ frame]));
+
+ // Turn fast resize mode on and change the view's frame. This time, the child
+ // should not resize, but it should be anchored to the top left.
+ [view_ setFastResizeMode:YES];
+ [view_ setFrame:NSMakeRect(15, 30, 250, 250)];
+ EXPECT_TRUE(NSEqualSizes([childView_ frame].size, squareFrame.size));
+ EXPECT_EQ(NSMinX([view_ bounds]), NSMinX([childView_ frame]));
+ EXPECT_EQ(NSMaxY([view_ bounds]), NSMaxY([childView_ frame]));
+
+ // Another resize with fast resize mode on.
+ [view_ setFrame:rectFrame];
+ EXPECT_TRUE(NSEqualSizes([childView_ frame].size, squareFrame.size));
+ EXPECT_EQ(NSMinX([view_ bounds]), NSMinX([childView_ frame]));
+ EXPECT_EQ(NSMaxY([view_ bounds]), NSMaxY([childView_ frame]));
+
+ // Turn fast resize mode off. This should initiate an immediate resize, even
+ // though we haven't called setFrame directly.
+ [view_ setFastResizeMode:NO];
+ EXPECT_TRUE(NSEqualRects([view_ frame], rectFrame));
+ EXPECT_TRUE(NSEqualRects([view_ bounds], [childView_ frame]));
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/file_metadata.h b/chrome/browser/ui/cocoa/file_metadata.h
new file mode 100644
index 0000000..2a6cfc5
--- /dev/null
+++ b/chrome/browser/ui/cocoa/file_metadata.h
@@ -0,0 +1,29 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_FILE_METADATA_H_
+#define CHROME_BROWSER_UI_COCOA_FILE_METADATA_H_
+#pragma once
+
+class FilePath;
+class GURL;
+
+namespace file_metadata {
+
+// Adds origin metadata to the file.
+// |source| should be the source URL for the download, and |referrer| should be
+// the URL the user initiated the download from.
+void AddOriginMetadataToFile(const FilePath& file, const GURL& source,
+ const GURL& referrer);
+
+// Adds quarantine metadata to the file, assuming it has already been
+// quarantined by the OS.
+// |source| should be the source URL for the download, and |referrer| should be
+// the URL the user initiated the download from.
+void AddQuarantineMetadataToFile(const FilePath& file, const GURL& source,
+ const GURL& referrer);
+
+} // namespace file_metadata
+
+#endif // CHROME_BROWSER_UI_COCOA_FILE_METADATA_H_
diff --git a/chrome/browser/ui/cocoa/file_metadata.mm b/chrome/browser/ui/cocoa/file_metadata.mm
new file mode 100644
index 0000000..d19e3ac
--- /dev/null
+++ b/chrome/browser/ui/cocoa/file_metadata.mm
@@ -0,0 +1,167 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/file_metadata.h"
+
+#include <ApplicationServices/ApplicationServices.h>
+#include <Foundation/Foundation.h>
+
+#include "base/file_path.h"
+#include "base/logging.h"
+#include "base/mac_util.h"
+#include "base/mac/scoped_cftyperef.h"
+#include "googleurl/src/gurl.h"
+
+namespace file_metadata {
+
+// As of Mac OS X 10.4 ("Tiger"), files can be tagged with metadata describing
+// various attributes. Metadata is integrated with the system's Spotlight
+// feature and is searchable. Ordinarily, metadata can only be set by
+// Spotlight importers, which requires that the importer own the target file.
+// However, there's an attribute intended to describe the origin of a
+// file, that can store the source URL and referrer of a downloaded file.
+// It's stored as a "com.apple.metadata:kMDItemWhereFroms" extended attribute,
+// structured as a binary1-format plist containing a list of sources. This
+// attribute can only be populated by the downloader, not a Spotlight importer.
+// Safari on 10.4 and later populates this attribute.
+//
+// With this metadata set, you can locate downloads by performing a Spotlight
+// search for their source or referrer URLs, either from within the Spotlight
+// UI or from the command line:
+// mdfind 'kMDItemWhereFroms == "http://releases.mozilla.org/*"'
+//
+// There is no documented API to set metadata on a file directly as of the
+// 10.5 SDK. The MDSetItemAttribute function does exist to perform this task,
+// but it's undocumented.
+void AddOriginMetadataToFile(const FilePath& file, const GURL& source,
+ const GURL& referrer) {
+ // There's no declaration for MDItemSetAttribute in any known public SDK.
+ // It exists in the 10.4 and 10.5 runtimes. To play it safe, do the lookup
+ // at runtime instead of declaring it ourselves and linking against what's
+ // provided. This has two benefits:
+ // - If Apple relents and declares the function in a future SDK (it's
+ // happened before), our build won't break.
+ // - If Apple removes or renames the function in a future runtime, the
+ // loader won't refuse to let the application launch. Instead, we'll
+ // silently fail to set any metadata.
+ typedef OSStatus (*MDItemSetAttribute_type)(MDItemRef, CFStringRef,
+ CFTypeRef);
+ static MDItemSetAttribute_type md_item_set_attribute_func = NULL;
+
+ static bool did_symbol_lookup = false;
+ if (!did_symbol_lookup) {
+ did_symbol_lookup = true;
+ CFBundleRef metadata_bundle =
+ CFBundleGetBundleWithIdentifier(CFSTR("com.apple.Metadata"));
+ if (!metadata_bundle)
+ return;
+
+ md_item_set_attribute_func = (MDItemSetAttribute_type)
+ CFBundleGetFunctionPointerForName(metadata_bundle,
+ CFSTR("MDItemSetAttribute"));
+ }
+ if (!md_item_set_attribute_func)
+ return;
+
+ NSString* file_path =
+ [NSString stringWithUTF8String:file.value().c_str()];
+ if (!file_path)
+ return;
+
+ base::mac::ScopedCFTypeRef<MDItemRef> md_item(
+ MDItemCreate(NULL, reinterpret_cast<CFStringRef>(file_path)));
+ if (!md_item)
+ return;
+
+ // We won't put any more than 2 items into the attribute.
+ NSMutableArray* list = [NSMutableArray arrayWithCapacity:2];
+
+ // Follow Safari's lead: the first item in the list is the source URL of
+ // the downloaded file. If the referrer is known, store that, too.
+ NSString* origin_url = [NSString stringWithUTF8String:source.spec().c_str()];
+ if (origin_url)
+ [list addObject:origin_url];
+ NSString* referrer_url =
+ [NSString stringWithUTF8String:referrer.spec().c_str()];
+ if (referrer_url)
+ [list addObject:referrer_url];
+
+ md_item_set_attribute_func(md_item, kMDItemWhereFroms,
+ reinterpret_cast<CFArrayRef>(list));
+}
+
+// The OS will automatically quarantine files due to the
+// LSFileQuarantineEnabled entry in our Info.plist, but it knows relatively
+// little about the files. We add more information about the download to
+// improve the UI shown by the OS when the users tries to open the file.
+void AddQuarantineMetadataToFile(const FilePath& file, const GURL& source,
+ const GURL& referrer) {
+ FSRef file_ref;
+ if (!mac_util::FSRefFromPath(file.value(), &file_ref))
+ return;
+
+ NSMutableDictionary* quarantine_properties = nil;
+ CFTypeRef quarantine_properties_base = NULL;
+ if (LSCopyItemAttribute(&file_ref, kLSRolesAll, kLSItemQuarantineProperties,
+ &quarantine_properties_base) == noErr) {
+ if (CFGetTypeID(quarantine_properties_base) ==
+ CFDictionaryGetTypeID()) {
+ // Quarantine properties will already exist if LSFileQuarantineEnabled
+ // is on and the file doesn't match an exclusion.
+ quarantine_properties =
+ [[(NSDictionary*)quarantine_properties_base mutableCopy] autorelease];
+ } else {
+ LOG(WARNING) << "kLSItemQuarantineProperties is not a dictionary on file "
+ << file.value();
+ }
+ CFRelease(quarantine_properties_base);
+ }
+
+ if (!quarantine_properties) {
+ // If there are no quarantine properties, then the file isn't quarantined
+ // (e.g., because the user has set up exclusions for certain file types).
+ // We don't want to add any metadata, because that will cause the file to
+ // be quarantined against the user's wishes.
+ return;
+ }
+
+ // kLSQuarantineAgentNameKey, kLSQuarantineAgentBundleIdentifierKey, and
+ // kLSQuarantineTimeStampKey are set for us (see LSQuarantine.h), so we only
+ // need to set the values that the OS can't infer.
+
+ if (![quarantine_properties valueForKey:(NSString*)kLSQuarantineTypeKey]) {
+ CFStringRef type = (source.SchemeIs("http") || source.SchemeIs("https"))
+ ? kLSQuarantineTypeWebDownload
+ : kLSQuarantineTypeOtherDownload;
+ [quarantine_properties setValue:(NSString*)type
+ forKey:(NSString*)kLSQuarantineTypeKey];
+ }
+
+ if (![quarantine_properties
+ valueForKey:(NSString*)kLSQuarantineOriginURLKey] &&
+ referrer.is_valid()) {
+ NSString* referrer_url =
+ [NSString stringWithUTF8String:referrer.spec().c_str()];
+ [quarantine_properties setValue:referrer_url
+ forKey:(NSString*)kLSQuarantineOriginURLKey];
+ }
+
+ if (![quarantine_properties valueForKey:(NSString*)kLSQuarantineDataURLKey] &&
+ source.is_valid()) {
+ NSString* origin_url =
+ [NSString stringWithUTF8String:source.spec().c_str()];
+ [quarantine_properties setValue:origin_url
+ forKey:(NSString*)kLSQuarantineDataURLKey];
+ }
+
+ OSStatus os_error = LSSetItemAttribute(&file_ref, kLSRolesAll,
+ kLSItemQuarantineProperties,
+ quarantine_properties);
+ if (os_error != noErr) {
+ LOG(WARNING) << "Unable to set quarantine attributes on file "
+ << file.value();
+ }
+}
+
+} // namespace file_metadata
diff --git a/chrome/browser/ui/cocoa/find_bar_bridge.h b/chrome/browser/ui/cocoa/find_bar_bridge.h
new file mode 100644
index 0000000..05a1516
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_bar_bridge.h
@@ -0,0 +1,95 @@
+// 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_UI_COCOA_FIND_BAR_BRIDGE_H_
+#define CHROME_BROWSER_UI_COCOA_FIND_BAR_BRIDGE_H_
+#pragma once
+
+#include "base/logging.h"
+#include "chrome/browser/find_bar.h"
+
+class BrowserWindowCocoa;
+class FindBarController;
+
+// This class is included by find_bar_host_browsertest.cc, so it has to be
+// objc-free.
+#ifdef __OBJC__
+@class FindBarCocoaController;
+#else
+class FindBarCocoaController;
+#endif
+
+// Implementation of FindBar for the Mac. This class simply passes
+// each message along to |cocoa_controller_|.
+//
+// The initialization here is a bit complicated. FindBarBridge is
+// created by a static method in BrowserWindow. The FindBarBridge
+// constructor creates a FindBarCocoaController, which in turn loads a
+// FindBarView from a nib file. All of this is happening outside of
+// the main view hierarchy, so the static method also calls
+// BrowserWindowCocoa::AddFindBar() in order to add its FindBarView to
+// the cocoa views hierarchy.
+//
+// Memory ownership is relatively straightforward. The FindBarBridge
+// object is owned by the Browser. FindBarCocoaController is retained
+// by bother FindBarBridge and BrowserWindowController, since both use it.
+
+class FindBarBridge : public FindBar,
+ public FindBarTesting {
+ public:
+ FindBarBridge();
+ virtual ~FindBarBridge();
+
+ FindBarCocoaController* find_bar_cocoa_controller() {
+ return cocoa_controller_;
+ }
+
+ virtual void SetFindBarController(FindBarController* find_bar_controller) {
+ find_bar_controller_ = find_bar_controller;
+ }
+
+ virtual FindBarController* GetFindBarController() const {
+ DCHECK(find_bar_controller_);
+ return find_bar_controller_;
+ }
+
+ virtual FindBarTesting* GetFindBarTesting() {
+ return this;
+ }
+
+ // Methods from FindBar.
+ virtual void Show(bool animate);
+ virtual void Hide(bool animate);
+ virtual void SetFocusAndSelection();
+ virtual void ClearResults(const FindNotificationDetails& results);
+ virtual void StopAnimation();
+ virtual void SetFindText(const string16& find_text);
+ virtual void UpdateUIForFindResult(const FindNotificationDetails& result,
+ const string16& find_text);
+ virtual void AudibleAlert();
+ virtual bool IsFindBarVisible();
+ virtual void RestoreSavedFocus();
+ virtual void MoveWindowIfNecessary(const gfx::Rect& selection_rect,
+ bool no_redraw);
+
+ // Methods from FindBarTesting.
+ virtual bool GetFindBarWindowInfo(gfx::Point* position,
+ bool* fully_visible);
+ virtual string16 GetFindText();
+
+ // Used to disable find bar animations when testing.
+ static bool disable_animations_during_testing_;
+
+ private:
+ // Pointer to the cocoa controller which manages the cocoa view. Is
+ // never nil.
+ FindBarCocoaController* cocoa_controller_;
+
+ // Pointer back to the owning controller.
+ FindBarController* find_bar_controller_; // weak, owns us
+
+ DISALLOW_COPY_AND_ASSIGN(FindBarBridge);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_FIND_BAR_BRIDGE_H_
diff --git a/chrome/browser/ui/cocoa/find_bar_bridge.mm b/chrome/browser/ui/cocoa/find_bar_bridge.mm
new file mode 100644
index 0000000..dc9146c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_bar_bridge.mm
@@ -0,0 +1,96 @@
+// 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 "chrome/browser/ui/cocoa/find_bar_bridge.h"
+
+#include "base/sys_string_conversions.h"
+#import "chrome/browser/ui/cocoa/find_bar_cocoa_controller.h"
+
+// static
+bool FindBarBridge::disable_animations_during_testing_ = false;
+
+FindBarBridge::FindBarBridge()
+ : find_bar_controller_(NULL) {
+ cocoa_controller_ = [[FindBarCocoaController alloc] init];
+ [cocoa_controller_ setFindBarBridge:this];
+}
+
+FindBarBridge::~FindBarBridge() {
+ [cocoa_controller_ release];
+}
+
+void FindBarBridge::Show(bool animate) {
+ bool really_animate = animate && !disable_animations_during_testing_;
+ [cocoa_controller_ showFindBar:(really_animate ? YES : NO)];
+}
+
+void FindBarBridge::Hide(bool animate) {
+ bool really_animate = animate && !disable_animations_during_testing_;
+ [cocoa_controller_ hideFindBar:(really_animate ? YES : NO)];
+}
+
+void FindBarBridge::SetFocusAndSelection() {
+ [cocoa_controller_ setFocusAndSelection];
+}
+
+void FindBarBridge::ClearResults(const FindNotificationDetails& results) {
+ [cocoa_controller_ clearResults:results];
+}
+
+void FindBarBridge::SetFindText(const string16& find_text) {
+ [cocoa_controller_ setFindText:base::SysUTF16ToNSString(find_text)];
+}
+
+void FindBarBridge::UpdateUIForFindResult(const FindNotificationDetails& result,
+ const string16& find_text) {
+ [cocoa_controller_ updateUIForFindResult:result withText:find_text];
+}
+
+void FindBarBridge::AudibleAlert() {
+ // Beep beep, beep beep, Yeah!
+ NSBeep();
+}
+
+bool FindBarBridge::IsFindBarVisible() {
+ return [cocoa_controller_ isFindBarVisible] ? true : false;
+}
+
+void FindBarBridge::MoveWindowIfNecessary(const gfx::Rect& selection_rect,
+ bool no_redraw) {
+ // http://crbug.com/11084
+ // http://crbug.com/22036
+}
+
+void FindBarBridge::StopAnimation() {
+ [cocoa_controller_ stopAnimation];
+}
+
+void FindBarBridge::RestoreSavedFocus() {
+ [cocoa_controller_ restoreSavedFocus];
+}
+
+bool FindBarBridge::GetFindBarWindowInfo(gfx::Point* position,
+ bool* fully_visible) {
+ // TODO(rohitrao): Return the proper position. http://crbug.com/22036
+ if (position)
+ *position = gfx::Point(0, 0);
+
+ NSWindow* window = [[cocoa_controller_ view] window];
+ bool window_visible = [window isVisible] ? true : false;
+ if (fully_visible) {
+ *fully_visible = window_visible &&
+ [cocoa_controller_ isFindBarVisible] &&
+ ![cocoa_controller_ isFindBarAnimating];
+ }
+ return window_visible;
+}
+
+string16 FindBarBridge::GetFindText() {
+ // This function is currently only used in Windows and Linux specific browser
+ // tests (testing prepopulate values that Mac's don't rely on), but if we add
+ // more tests that are non-platform specific, we need to flesh out this
+ // function.
+ NOTIMPLEMENTED();
+ return string16();
+}
diff --git a/chrome/browser/ui/cocoa/find_bar_bridge_unittest.mm b/chrome/browser/ui/cocoa/find_bar_bridge_unittest.mm
new file mode 100644
index 0000000..81679fdb
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_bar_bridge_unittest.mm
@@ -0,0 +1,30 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/find_bar_controller.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/find_bar_bridge.h"
+
+namespace {
+
+class FindBarBridgeTest : public CocoaTest {
+};
+
+TEST_F(FindBarBridgeTest, Creation) {
+ // Make sure the FindBarBridge constructor doesn't crash and
+ // properly initializes its FindBarCocoaController.
+ FindBarBridge bridge;
+ EXPECT_TRUE(bridge.find_bar_cocoa_controller() != NULL);
+}
+
+TEST_F(FindBarBridgeTest, Accessors) {
+ // Get/SetFindBarController are virtual methods implemented in
+ // FindBarBridge, so we test them here.
+ FindBarBridge* bridge = new FindBarBridge();
+ FindBarController controller(bridge); // takes ownership of |bridge|.
+ bridge->SetFindBarController(&controller);
+
+ EXPECT_EQ(&controller, bridge->GetFindBarController());
+}
+} // namespace
diff --git a/chrome/browser/ui/cocoa/find_bar_cocoa_controller.h b/chrome/browser/ui/cocoa/find_bar_cocoa_controller.h
new file mode 100644
index 0000000..289d89c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_bar_cocoa_controller.h
@@ -0,0 +1,78 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/find_bar_cocoa_controller.h"
+
+#include "base/scoped_nsobject.h"
+#include "base/string16.h"
+
+class BrowserWindowCocoa;
+class FindBarBridge;
+@class FindBarTextField;
+class FindNotificationDetails;
+@class FocusTracker;
+
+// A controller for the find bar in the browser window. Manages
+// updating the state of the find bar and provides a target for the
+// next/previous/close buttons. Certain operations require a pointer
+// to the cross-platform FindBarController, so be sure to call
+// setFindBarBridge: after creating this controller.
+
+@interface FindBarCocoaController : NSViewController {
+ @private
+ IBOutlet NSView* findBarView_;
+ IBOutlet FindBarTextField* findText_;
+ IBOutlet NSButton* nextButton_;
+ IBOutlet NSButton* previousButton_;
+
+ // Needed to call methods on FindBarController.
+ FindBarBridge* findBarBridge_; // weak
+
+ scoped_nsobject<FocusTracker> focusTracker_;
+
+ // The currently-running animation. This is defined to be non-nil if an
+ // animation is running, and is always nil otherwise. The
+ // FindBarCocoaController should not be deallocated while an animation is
+ // running (stopAnimation is currently called before the last tab in a
+ // window is removed).
+ scoped_nsobject<NSViewAnimation> currentAnimation_;
+
+ // If YES, do nothing as a result of find pasteboard update notifications.
+ BOOL suppressPboardUpdateActions_;
+};
+
+// Initializes a new FindBarCocoaController.
+- (id)init;
+
+- (void)setFindBarBridge:(FindBarBridge*)findBar;
+
+- (IBAction)close:(id)sender;
+
+- (IBAction)nextResult:(id)sender;
+
+- (IBAction)previousResult:(id)sender;
+
+// Position the find bar at the given maximum y-coordinate (the min-y of the
+// bar -- toolbar + possibly bookmark bar, but not including the infobars) with
+// the given maximum width (i.e., the find bar should fit between 0 and
+// |maxWidth|).
+- (void)positionFindBarViewAtMaxY:(CGFloat)maxY maxWidth:(CGFloat)maxWidth;
+
+// Methods called from FindBarBridge.
+- (void)showFindBar:(BOOL)animate;
+- (void)hideFindBar:(BOOL)animate;
+- (void)stopAnimation;
+- (void)setFocusAndSelection;
+- (void)restoreSavedFocus;
+- (void)setFindText:(NSString*)findText;
+
+- (void)clearResults:(const FindNotificationDetails&)results;
+- (void)updateUIForFindResult:(const FindNotificationDetails&)results
+ withText:(const string16&)findText;
+- (BOOL)isFindBarVisible;
+- (BOOL)isFindBarAnimating;
+
+@end
diff --git a/chrome/browser/ui/cocoa/find_bar_cocoa_controller.mm b/chrome/browser/ui/cocoa/find_bar_cocoa_controller.mm
new file mode 100644
index 0000000..f50d6db
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_bar_cocoa_controller.mm
@@ -0,0 +1,384 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/find_bar_controller.h"
+#include "chrome/browser/renderer_host/render_view_host.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/ui/cocoa/browser_window_cocoa.h"
+#import "chrome/browser/ui/cocoa/find_bar_cocoa_controller.h"
+#import "chrome/browser/ui/cocoa/find_bar_bridge.h"
+#import "chrome/browser/ui/cocoa/find_bar_text_field.h"
+#import "chrome/browser/ui/cocoa/find_bar_text_field_cell.h"
+#import "chrome/browser/ui/cocoa/find_pasteboard.h"
+#import "chrome/browser/ui/cocoa/focus_tracker.h"
+#import "chrome/browser/ui/cocoa/tab_strip_controller.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+
+namespace {
+const float kFindBarOpenDuration = 0.2;
+const float kFindBarCloseDuration = 0.15;
+}
+
+@interface FindBarCocoaController (PrivateMethods)
+// Returns the appropriate frame for a hidden find bar.
+- (NSRect)hiddenFindBarFrame;
+
+// Sets the frame of |findBarView_|. |duration| is ignored if |animate| is NO.
+- (void)setFindBarFrame:(NSRect)endFrame
+ animate:(BOOL)animate
+ duration:(float)duration;
+
+// Optionally stops the current search, puts |text| into the find bar, and
+// enables the buttons, but doesn't start a new search for |text|.
+- (void)prepopulateText:(NSString*)text stopSearch:(BOOL)stopSearch;
+@end
+
+@implementation FindBarCocoaController
+
+- (id)init {
+ if ((self = [super initWithNibName:@"FindBar"
+ bundle:mac_util::MainAppBundle()])) {
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(findPboardUpdated:)
+ name:kFindPasteboardChangedNotification
+ object:[FindPasteboard sharedInstance]];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ // All animations should be explicitly stopped by the TabContents before a tab
+ // is closed.
+ DCHECK(!currentAnimation_.get());
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+- (void)setFindBarBridge:(FindBarBridge*)findBarBridge {
+ DCHECK(!findBarBridge_); // should only be called once.
+ findBarBridge_ = findBarBridge;
+}
+
+- (void)awakeFromNib {
+ [findBarView_ setFrame:[self hiddenFindBarFrame]];
+
+ // Stopping the search requires a findbar controller, which isn't valid yet
+ // during setup. Furthermore, there is no active search yet anyway.
+ [self prepopulateText:[[FindPasteboard sharedInstance] findText]
+ stopSearch:NO];
+}
+
+- (IBAction)close:(id)sender {
+ if (findBarBridge_)
+ findBarBridge_->GetFindBarController()->EndFindSession(
+ FindBarController::kKeepSelection);
+}
+
+- (IBAction)previousResult:(id)sender {
+ if (findBarBridge_)
+ findBarBridge_->GetFindBarController()->tab_contents()->StartFinding(
+ base::SysNSStringToUTF16([findText_ stringValue]),
+ false, false);
+}
+
+- (IBAction)nextResult:(id)sender {
+ if (findBarBridge_)
+ findBarBridge_->GetFindBarController()->tab_contents()->StartFinding(
+ base::SysNSStringToUTF16([findText_ stringValue]),
+ true, false);
+}
+
+- (void)findPboardUpdated:(NSNotification*)notification {
+ if (suppressPboardUpdateActions_)
+ return;
+ [self prepopulateText:[[FindPasteboard sharedInstance] findText]
+ stopSearch:YES];
+}
+
+- (void)positionFindBarViewAtMaxY:(CGFloat)maxY maxWidth:(CGFloat)maxWidth {
+ static const CGFloat kRightEdgeOffset = 25;
+ NSView* containerView = [self view];
+ CGFloat containerHeight = NSHeight([containerView frame]);
+ CGFloat containerWidth = NSWidth([containerView frame]);
+
+ // Adjust where we'll actually place the find bar.
+ CGFloat maxX = maxWidth - kRightEdgeOffset;
+ DLOG_IF(WARNING, maxX < 0) << "Window too narrow for find bar";
+ maxY += 1;
+
+ NSRect newFrame = NSMakeRect(maxX - containerWidth, maxY - containerHeight,
+ containerWidth, containerHeight);
+ [containerView setFrame:newFrame];
+}
+
+// NSControl delegate method.
+- (void)controlTextDidChange:(NSNotification *)aNotification {
+ if (!findBarBridge_)
+ return;
+
+ TabContents* tab_contents =
+ findBarBridge_->GetFindBarController()->tab_contents();
+ if (!tab_contents)
+ return;
+
+ NSString* findText = [findText_ stringValue];
+ suppressPboardUpdateActions_ = YES;
+ [[FindPasteboard sharedInstance] setFindText:findText];
+ suppressPboardUpdateActions_ = NO;
+
+ if ([findText length] > 0) {
+ tab_contents->StartFinding(base::SysNSStringToUTF16(findText), true, false);
+ } else {
+ // The textbox is empty so we reset.
+ tab_contents->StopFinding(FindBarController::kClearSelection);
+ [self updateUIForFindResult:tab_contents->find_result()
+ withText:string16()];
+ }
+}
+
+// NSControl delegate method
+- (BOOL)control:(NSControl*)control
+ textView:(NSTextView*)textView
+ doCommandBySelector:(SEL)command {
+ if (command == @selector(insertNewline:)) {
+ // Pressing Return
+ NSEvent* event = [NSApp currentEvent];
+
+ if ([event modifierFlags] & NSShiftKeyMask)
+ [previousButton_ performClick:nil];
+ else
+ [nextButton_ performClick:nil];
+
+ return YES;
+ } else if (command == @selector(insertLineBreak:)) {
+ // Pressing Ctrl-Return
+ if (findBarBridge_) {
+ findBarBridge_->GetFindBarController()->EndFindSession(
+ FindBarController::kActivateSelection);
+ }
+ return YES;
+ } else if (command == @selector(pageUp:) ||
+ command == @selector(pageUpAndModifySelection:) ||
+ command == @selector(scrollPageUp:) ||
+ command == @selector(pageDown:) ||
+ command == @selector(pageDownAndModifySelection:) ||
+ command == @selector(scrollPageDown:) ||
+ command == @selector(scrollToBeginningOfDocument:) ||
+ command == @selector(scrollToEndOfDocument:) ||
+ command == @selector(moveUp:) ||
+ command == @selector(moveDown:)) {
+ TabContents* contents =
+ findBarBridge_->GetFindBarController()->tab_contents();
+ if (!contents)
+ return NO;
+
+ // Sanity-check to make sure we got a keyboard event.
+ NSEvent* event = [NSApp currentEvent];
+ if ([event type] != NSKeyDown && [event type] != NSKeyUp)
+ return NO;
+
+ // Forward the event to the renderer.
+ // TODO(rohitrao): Should this call -[BaseView keyEvent:]? Is there code in
+ // that function that we want to keep or avoid? Calling
+ // |ForwardKeyboardEvent()| directly ignores edit commands, which breaks
+ // cmd-up/down if we ever decide to include |moveToBeginningOfDocument:| in
+ // the list above.
+ RenderViewHost* render_view_host = contents->render_view_host();
+ render_view_host->ForwardKeyboardEvent(NativeWebKeyboardEvent(event));
+ return YES;
+ }
+
+ return NO;
+}
+
+// Methods from FindBar
+- (void)showFindBar:(BOOL)animate {
+ // Save the currently-focused view. |findBarView_| is in the view
+ // hierarchy by now. showFindBar can be called even when the
+ // findbar is already open, so do not overwrite an already saved
+ // view.
+ if (!focusTracker_.get())
+ focusTracker_.reset(
+ [[FocusTracker alloc] initWithWindow:[findBarView_ window]]);
+
+ // Animate the view into place.
+ NSRect frame = [findBarView_ frame];
+ frame.origin = NSMakePoint(0, 0);
+ [self setFindBarFrame:frame animate:animate duration:kFindBarOpenDuration];
+}
+
+- (void)hideFindBar:(BOOL)animate {
+ NSRect frame = [self hiddenFindBarFrame];
+ [self setFindBarFrame:frame animate:animate duration:kFindBarCloseDuration];
+}
+
+- (void)stopAnimation {
+ if (currentAnimation_.get()) {
+ [currentAnimation_ stopAnimation];
+ currentAnimation_.reset(nil);
+ }
+}
+
+- (void)setFocusAndSelection {
+ [[findText_ window] makeFirstResponder:findText_];
+
+ // Enable the buttons if the find text is non-empty.
+ BOOL buttonsEnabled = ([[findText_ stringValue] length] > 0) ? YES : NO;
+ [previousButton_ setEnabled:buttonsEnabled];
+ [nextButton_ setEnabled:buttonsEnabled];
+}
+
+- (void)restoreSavedFocus {
+ if (!(focusTracker_.get() &&
+ [focusTracker_ restoreFocusInWindow:[findBarView_ window]])) {
+ // Fall back to giving focus to the tab contents.
+ findBarBridge_->GetFindBarController()->tab_contents()->Focus();
+ }
+ focusTracker_.reset(nil);
+}
+
+- (void)setFindText:(NSString*)findText {
+ [findText_ setStringValue:findText];
+
+ // Make sure the text in the find bar always ends up in the find pasteboard
+ // (and, via notifications, in the other find bars too).
+ [[FindPasteboard sharedInstance] setFindText:findText];
+}
+
+- (void)clearResults:(const FindNotificationDetails&)results {
+ // Just call updateUIForFindResult, which will take care of clearing
+ // the search text and the results label.
+ [self updateUIForFindResult:results withText:string16()];
+}
+
+- (void)updateUIForFindResult:(const FindNotificationDetails&)result
+ withText:(const string16&)findText {
+ // If we don't have any results and something was passed in, then
+ // that means someone pressed Cmd-G while the Find box was
+ // closed. In that case we need to repopulate the Find box with what
+ // was passed in.
+ if ([[findText_ stringValue] length] == 0 && !findText.empty()) {
+ [findText_ setStringValue:base::SysUTF16ToNSString(findText)];
+ [findText_ selectText:self];
+ }
+
+ // Make sure Find Next and Find Previous are enabled if we found any matches.
+ BOOL buttonsEnabled = result.number_of_matches() > 0 ? YES : NO;
+ [previousButton_ setEnabled:buttonsEnabled];
+ [nextButton_ setEnabled:buttonsEnabled];
+
+ // Update the results label.
+ BOOL validRange = result.active_match_ordinal() != -1 &&
+ result.number_of_matches() != -1;
+ NSString* searchString = [findText_ stringValue];
+ if ([searchString length] > 0 && validRange) {
+ [[findText_ findBarTextFieldCell]
+ setActiveMatch:result.active_match_ordinal()
+ of:result.number_of_matches()];
+ } else {
+ // If there was no text entered, we don't show anything in the results area.
+ [[findText_ findBarTextFieldCell] clearResults];
+ }
+
+ [findText_ resetFieldEditorFrameIfNeeded];
+
+ // If we found any results, reset the focus tracker, so we always
+ // restore focus to the tab contents.
+ if (result.number_of_matches() > 0)
+ focusTracker_.reset(nil);
+}
+
+- (BOOL)isFindBarVisible {
+ // Find bar is visible if any part of it is on the screen.
+ return NSIntersectsRect([[self view] bounds], [findBarView_ frame]);
+}
+
+- (BOOL)isFindBarAnimating {
+ return (currentAnimation_.get() != nil);
+}
+
+// NSAnimation delegate methods.
+- (void)animationDidEnd:(NSAnimation*)animation {
+ // Autorelease the animation (cannot use release because the animation object
+ // is still on the stack.
+ DCHECK(animation == currentAnimation_.get());
+ [currentAnimation_.release() autorelease];
+
+ // If the find bar is not visible, make it actually hidden, so it'll no longer
+ // respond to key events.
+ [findBarView_ setHidden:![self isFindBarVisible]];
+}
+
+@end
+
+@implementation FindBarCocoaController (PrivateMethods)
+
+- (NSRect)hiddenFindBarFrame {
+ NSRect frame = [findBarView_ frame];
+ NSRect containerBounds = [[self view] bounds];
+ frame.origin = NSMakePoint(NSMinX(containerBounds), NSMaxY(containerBounds));
+ return frame;
+}
+
+- (void)setFindBarFrame:(NSRect)endFrame
+ animate:(BOOL)animate
+ duration:(float)duration {
+ // Save the current frame.
+ NSRect startFrame = [findBarView_ frame];
+
+ // Stop any existing animations.
+ [currentAnimation_ stopAnimation];
+
+ if (!animate) {
+ [findBarView_ setFrame:endFrame];
+ [findBarView_ setHidden:![self isFindBarVisible]];
+ currentAnimation_.reset(nil);
+ return;
+ }
+
+ // If animating, ensure that the find bar is not hidden. Hidden status will be
+ // updated at the end of the animation.
+ [findBarView_ setHidden:NO];
+
+ // Reset the frame to what was saved above.
+ [findBarView_ setFrame:startFrame];
+ NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:
+ findBarView_, NSViewAnimationTargetKey,
+ [NSValue valueWithRect:endFrame], NSViewAnimationEndFrameKey, nil];
+
+ currentAnimation_.reset(
+ [[NSViewAnimation alloc]
+ initWithViewAnimations:[NSArray arrayWithObjects:dict, nil]]);
+ [currentAnimation_ gtm_setDuration:duration
+ eventMask:NSLeftMouseUpMask];
+ [currentAnimation_ setDelegate:self];
+ [currentAnimation_ startAnimation];
+}
+
+- (void)prepopulateText:(NSString*)text stopSearch:(BOOL)stopSearch{
+ [self setFindText:text];
+
+ // End the find session, hide the "x of y" text and disable the
+ // buttons, but do not close the find bar or raise the window here.
+ if (stopSearch && findBarBridge_) {
+ TabContents* contents =
+ findBarBridge_->GetFindBarController()->tab_contents();
+ if (contents) {
+ contents->StopFinding(FindBarController::kClearSelection);
+ findBarBridge_->ClearResults(contents->find_result());
+ }
+ }
+
+ // Has to happen after |ClearResults()| above.
+ BOOL buttonsEnabled = [text length] > 0 ? YES : NO;
+ [previousButton_ setEnabled:buttonsEnabled];
+ [nextButton_ setEnabled:buttonsEnabled];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/find_bar_cocoa_controller_unittest.mm b/chrome/browser/ui/cocoa/find_bar_cocoa_controller_unittest.mm
new file mode 100644
index 0000000..ca85dd2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_bar_cocoa_controller_unittest.mm
@@ -0,0 +1,138 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/browser_window.h"
+#include "chrome/browser/find_notification_details.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/find_bar_cocoa_controller.h"
+#import "chrome/browser/ui/cocoa/find_pasteboard.h"
+#import "chrome/browser/ui/cocoa/find_bar_text_field.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+// Expose private variables to make testing easier.
+@interface FindBarCocoaController(Testing)
+- (NSView*)findBarView;
+- (NSString*)findText;
+- (FindBarTextField*)findTextField;
+@end
+
+@implementation FindBarCocoaController(Testing)
+- (NSView*)findBarView {
+ return findBarView_;
+}
+
+- (NSString*)findText {
+ return [findText_ stringValue];
+}
+
+- (FindBarTextField*)findTextField {
+ return findText_;
+}
+
+- (NSButton*)nextButton {
+ return nextButton_;
+}
+
+- (NSButton*)previousButton {
+ return previousButton_;
+}
+@end
+
+namespace {
+
+class FindBarCocoaControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ controller_.reset([[FindBarCocoaController alloc] init]);
+ [[test_window() contentView] addSubview:[controller_ view]];
+ }
+
+ protected:
+ scoped_nsobject<FindBarCocoaController> controller_;
+};
+
+TEST_VIEW(FindBarCocoaControllerTest, [controller_ view])
+
+TEST_F(FindBarCocoaControllerTest, ImagesLoadedProperly) {
+ EXPECT_TRUE([[[controller_ nextButton] image] isValid]);
+ EXPECT_TRUE([[[controller_ previousButton] image] isValid]);
+}
+
+TEST_F(FindBarCocoaControllerTest, ShowAndHide) {
+ NSView* findBarView = [controller_ findBarView];
+
+ ASSERT_GT([findBarView frame].origin.y, 0);
+ ASSERT_FALSE([controller_ isFindBarVisible]);
+
+ [controller_ showFindBar:NO];
+ EXPECT_EQ([findBarView frame].origin.y, 0);
+ EXPECT_TRUE([controller_ isFindBarVisible]);
+
+ [controller_ hideFindBar:NO];
+ EXPECT_GT([findBarView frame].origin.y, 0);
+ EXPECT_FALSE([controller_ isFindBarVisible]);
+}
+
+TEST_F(FindBarCocoaControllerTest, SetFindText) {
+ NSTextField* findTextField = [controller_ findTextField];
+
+ // Start by making the find bar visible.
+ [controller_ showFindBar:NO];
+ EXPECT_TRUE([controller_ isFindBarVisible]);
+
+ // Set the find text.
+ NSString* const kFindText = @"Google";
+ [controller_ setFindText:kFindText];
+ EXPECT_EQ(
+ NSOrderedSame,
+ [[findTextField stringValue] compare:kFindText]);
+
+ // Call clearResults, which doesn't actually clear the find text but
+ // simply sets it back to what it was before. This is silly, but
+ // matches the behavior on other platforms. |details| isn't used by
+ // our implementation of clearResults, so it's ok to pass in an
+ // empty |details|.
+ FindNotificationDetails details;
+ [controller_ clearResults:details];
+ EXPECT_EQ(
+ NSOrderedSame,
+ [[findTextField stringValue] compare:kFindText]);
+}
+
+TEST_F(FindBarCocoaControllerTest, ResultLabelUpdatesCorrectly) {
+ // TODO(rohitrao): Test this. It may involve creating some dummy
+ // FindNotificationDetails objects.
+}
+
+TEST_F(FindBarCocoaControllerTest, FindTextIsGlobal) {
+ scoped_nsobject<FindBarCocoaController> otherController(
+ [[FindBarCocoaController alloc] init]);
+ [[test_window() contentView] addSubview:[otherController view]];
+
+ // Setting the text in one controller should update the other controller's
+ // text as well.
+ NSString* const kFindText = @"Respect to the man in the ice cream van";
+ [controller_ setFindText:kFindText];
+ EXPECT_EQ(
+ NSOrderedSame,
+ [[controller_ findText] compare:kFindText]);
+ EXPECT_EQ(
+ NSOrderedSame,
+ [[otherController.get() findText] compare:kFindText]);
+}
+
+TEST_F(FindBarCocoaControllerTest, SettingFindTextUpdatesFindPboard) {
+ NSString* const kFindText =
+ @"It's not a bird, it's not a plane, it must be Dave who's on the train";
+ [controller_ setFindText:kFindText];
+ EXPECT_EQ(
+ NSOrderedSame,
+ [[[FindPasteboard sharedInstance] findText] compare:kFindText]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/find_bar_text_field.h b/chrome/browser/ui/cocoa/find_bar_text_field.h
new file mode 100644
index 0000000..765cc64
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_bar_text_field.h
@@ -0,0 +1,21 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+#import "chrome/browser/ui/cocoa/styled_text_field.h"
+
+@class FindBarTextFieldCell;
+
+// TODO(rohitrao): This class may not need to exist, since it does not really
+// add any functionality over StyledTextField. See if we can change the nib
+// file to put a FindBarTextFieldCell into a StyledTextField.
+
+// Extends StyledTextField to use a custom cell class (FindBarTextFieldCell).
+@interface FindBarTextField : StyledTextField {
+}
+
+// Convenience method to return the cell, casted appropriately.
+- (FindBarTextFieldCell*)findBarTextFieldCell;
+
+@end
diff --git a/chrome/browser/ui/cocoa/find_bar_text_field.mm b/chrome/browser/ui/cocoa/find_bar_text_field.mm
new file mode 100644
index 0000000..805fca3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_bar_text_field.mm
@@ -0,0 +1,40 @@
+// 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/ui/cocoa/find_bar_text_field.h"
+
+#include "base/logging.h"
+#import "chrome/browser/ui/cocoa/find_bar_text_field_cell.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+
+@implementation FindBarTextField
+
++ (Class)cellClass {
+ return [FindBarTextFieldCell class];
+}
+
+- (void)awakeFromNib {
+ DCHECK([[self cell] isKindOfClass:[FindBarTextFieldCell class]]);
+
+ [self registerForDraggedTypes:
+ [NSArray arrayWithObjects:NSStringPboardType, nil]];
+}
+
+- (FindBarTextFieldCell*)findBarTextFieldCell {
+ DCHECK([[self cell] isKindOfClass:[FindBarTextFieldCell class]]);
+ return static_cast<FindBarTextFieldCell*>([self cell]);
+}
+
+- (ViewID)viewID {
+ return VIEW_ID_FIND_IN_PAGE_TEXT_FIELD;
+}
+
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info {
+ // When a drag enters the text field, focus the field. This will swap in the
+ // field editor, which will then handle the drag itself.
+ [[self window] makeFirstResponder:self];
+ return NSDragOperationNone;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/find_bar_text_field_cell.h b/chrome/browser/ui/cocoa/find_bar_text_field_cell.h
new file mode 100644
index 0000000..ee6785f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_bar_text_field_cell.h
@@ -0,0 +1,24 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/styled_text_field_cell.h"
+
+#include "base/scoped_nsobject.h"
+
+// FindBarTextFieldCell extends StyledTextFieldCell to provide support for a
+// results label rooted at the right edge of the cell.
+@interface FindBarTextFieldCell : StyledTextFieldCell {
+ @private
+ // Set if there is a results label to display on the right side of the cell.
+ scoped_nsobject<NSAttributedString> resultsString_;
+}
+
+// Sets the results label to the localized equivalent of "X of Y".
+- (void)setActiveMatch:(NSInteger)current of:(NSInteger)total;
+
+- (void)clearResults;
+
+@end
diff --git a/chrome/browser/ui/cocoa/find_bar_text_field_cell.mm b/chrome/browser/ui/cocoa/find_bar_text_field_cell.mm
new file mode 100644
index 0000000..3f93766
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_bar_text_field_cell.mm
@@ -0,0 +1,119 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/find_bar_text_field_cell.h"
+
+#include "app/l10n_util.h"
+#include "base/logging.h"
+#include "base/string_number_conversions.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "grit/generated_resources.h"
+
+namespace {
+
+const CGFloat kBaselineAdjust = 1.0;
+
+// How far to offset the keyword token into the field.
+const NSInteger kResultsXOffset = 3;
+
+// How much width (beyond text) to add to the keyword token on each
+// side.
+const NSInteger kResultsTokenInset = 3;
+
+// How far to shift bounding box of hint down from top of field.
+// Assumes -setFlipped:YES.
+const NSInteger kResultsYOffset = 4;
+
+// How far the editor insets itself, for purposes of determining if
+// decorations need to be trimmed.
+const CGFloat kEditorHorizontalInset = 3.0;
+
+// Conveniences to centralize width+offset calculations.
+CGFloat WidthForResults(NSAttributedString* resultsString) {
+ return kResultsXOffset + ceil([resultsString size].width) +
+ 2 * kResultsTokenInset;
+}
+
+} // namespace
+
+@implementation FindBarTextFieldCell
+
+- (CGFloat)baselineAdjust {
+ return kBaselineAdjust;
+}
+
+- (CGFloat)cornerRadius {
+ return 4.0;
+}
+
+- (StyledTextFieldCellRoundedFlags)roundedFlags {
+ return StyledTextFieldCellRoundedLeft;
+}
+
+// @synthesize doesn't seem to compile for this transition.
+- (NSAttributedString*)resultsString {
+ return resultsString_.get();
+}
+
+// Convenience for the attributes used in the right-justified info
+// cells. Sets the background color to red if |foundMatches| is YES.
+- (NSDictionary*)resultsAttributes:(BOOL)foundMatches {
+ scoped_nsobject<NSMutableParagraphStyle> style(
+ [[NSMutableParagraphStyle alloc] init]);
+ [style setAlignment:NSRightTextAlignment];
+
+ return [NSDictionary dictionaryWithObjectsAndKeys:
+ [self font], NSFontAttributeName,
+ [NSColor lightGrayColor], NSForegroundColorAttributeName,
+ [NSColor whiteColor], NSBackgroundColorAttributeName,
+ style.get(), NSParagraphStyleAttributeName,
+ nil];
+}
+
+- (void)setActiveMatch:(NSInteger)current of:(NSInteger)total {
+ NSString* results =
+ base::SysUTF16ToNSString(l10n_util::GetStringFUTF16(
+ IDS_FIND_IN_PAGE_COUNT,
+ base::IntToString16(current),
+ base::IntToString16(total)));
+ resultsString_.reset([[NSAttributedString alloc]
+ initWithString:results
+ attributes:[self resultsAttributes:(total > 0)]]);
+}
+
+- (void)clearResults {
+ resultsString_.reset(nil);
+}
+
+- (NSRect)textFrameForFrame:(NSRect)cellFrame {
+ NSRect textFrame([super textFrameForFrame:cellFrame]);
+ if (resultsString_)
+ textFrame.size.width -= WidthForResults(resultsString_);
+ return textFrame;
+}
+
+// Do not show the I-beam cursor over the results label.
+- (NSRect)textCursorFrameForFrame:(NSRect)cellFrame {
+ return [self textFrameForFrame:cellFrame];
+}
+
+- (void)drawResultsWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
+ DCHECK(resultsString_);
+
+ NSRect textFrame = [self textFrameForFrame:cellFrame];
+ NSRect infoFrame(NSMakeRect(NSMaxX(textFrame),
+ cellFrame.origin.y + kResultsYOffset,
+ ceil([resultsString_ size].width),
+ cellFrame.size.height - kResultsYOffset));
+ [resultsString_.get() drawInRect:infoFrame];
+}
+
+- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
+ if (resultsString_)
+ [self drawResultsWithFrame:cellFrame inView:controlView];
+ [super drawInteriorWithFrame:cellFrame inView:controlView];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/find_bar_text_field_cell_unittest.mm b/chrome/browser/ui/cocoa/find_bar_text_field_cell_unittest.mm
new file mode 100644
index 0000000..1448632
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_bar_text_field_cell_unittest.mm
@@ -0,0 +1,135 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/find_bar_text_field_cell.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface FindBarTextFieldCell (ExposedForTesting)
+- (NSAttributedString*)resultsString;
+@end
+
+@implementation FindBarTextFieldCell (ExposedForTesting)
+- (NSAttributedString*)resultsString {
+ return resultsString_.get();
+}
+@end
+
+namespace {
+
+// Width of the field so that we don't have to ask |field_| for it all
+// the time.
+const CGFloat kWidth(300.0);
+
+// A narrow width for tests which test things that don't fit.
+const CGFloat kNarrowWidth(5.0);
+
+class FindBarTextFieldCellTest : public CocoaTest {
+ public:
+ FindBarTextFieldCellTest() {
+ // Make sure this is wide enough to play games with the cell
+ // decorations.
+ const NSRect frame = NSMakeRect(0, 0, kWidth, 30);
+
+ scoped_nsobject<FindBarTextFieldCell> cell(
+ [[FindBarTextFieldCell alloc] initTextCell:@"Testing"]);
+ cell_ = cell;
+ [cell_ setEditable:YES];
+ [cell_ setBordered:YES];
+
+ scoped_nsobject<NSTextField> view(
+ [[NSTextField alloc] initWithFrame:frame]);
+ view_ = view;
+ [view_ setCell:cell_];
+
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ NSTextField* view_;
+ FindBarTextFieldCell* cell_;
+};
+
+// Basic view tests (AddRemove, Display).
+TEST_VIEW(FindBarTextFieldCellTest, view_);
+
+// Test drawing, mostly to ensure nothing leaks or crashes.
+TEST_F(FindBarTextFieldCellTest, FocusedDisplay) {
+ [view_ display];
+
+ // Test focused drawing.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:view_];
+ [view_ display];
+ [test_window() clearPretendKeyWindowAndFirstResponder];
+
+ // Test display of various cell configurations.
+ [cell_ setActiveMatch:4 of:30];
+ [view_ display];
+
+ [cell_ setActiveMatch:0 of:0];
+ [view_ display];
+
+ [cell_ clearResults];
+ [view_ display];
+}
+
+// Verify that setting and clearing the find results changes the results string
+// appropriately.
+TEST_F(FindBarTextFieldCellTest, SetAndClearFindResults) {
+ [cell_ setActiveMatch:10 of:30];
+ scoped_nsobject<NSAttributedString> tenString([[cell_ resultsString] copy]);
+ EXPECT_GT([tenString length], 0U);
+
+ [cell_ setActiveMatch:0 of:0];
+ scoped_nsobject<NSAttributedString> zeroString([[cell_ resultsString] copy]);
+ EXPECT_GT([zeroString length], 0U);
+ EXPECT_FALSE([tenString isEqualToAttributedString:zeroString]);
+
+ [cell_ clearResults];
+ EXPECT_EQ(0U, [[cell_ resultsString] length]);
+}
+
+TEST_F(FindBarTextFieldCellTest, TextFrame) {
+ const NSRect bounds = [view_ bounds];
+ NSRect textFrame = [cell_ textFrameForFrame:bounds];
+ NSRect cursorFrame = [cell_ textCursorFrameForFrame:bounds];
+
+ // At default settings, everything goes to the text area.
+ EXPECT_FALSE(NSIsEmptyRect(textFrame));
+ EXPECT_TRUE(NSContainsRect(bounds, textFrame));
+ EXPECT_EQ(NSMinX(bounds), NSMinX(textFrame));
+ EXPECT_EQ(NSMaxX(bounds), NSMaxX(textFrame));
+ EXPECT_TRUE(NSEqualRects(cursorFrame, textFrame));
+
+ // Setting an active match leaves text frame to left.
+ [cell_ setActiveMatch:4 of:5];
+ textFrame = [cell_ textFrameForFrame:bounds];
+ cursorFrame = [cell_ textCursorFrameForFrame:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(textFrame));
+ EXPECT_TRUE(NSContainsRect(bounds, textFrame));
+ EXPECT_LT(NSMaxX(textFrame), NSMaxX(bounds));
+ EXPECT_TRUE(NSEqualRects(cursorFrame, textFrame));
+
+}
+
+// The editor frame should be slightly inset from the text frame.
+TEST_F(FindBarTextFieldCellTest, DrawingRectForBounds) {
+ const NSRect bounds = [view_ bounds];
+ NSRect textFrame = [cell_ textFrameForFrame:bounds];
+ NSRect drawingRect = [cell_ drawingRectForBounds:bounds];
+
+ EXPECT_FALSE(NSIsEmptyRect(drawingRect));
+ EXPECT_TRUE(NSContainsRect(textFrame, NSInsetRect(drawingRect, 1, 1)));
+
+ [cell_ setActiveMatch:4 of:5];
+ textFrame = [cell_ textFrameForFrame:bounds];
+ drawingRect = [cell_ drawingRectForBounds:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(drawingRect));
+ EXPECT_TRUE(NSContainsRect(textFrame, NSInsetRect(drawingRect, 1, 1)));
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/find_bar_text_field_unittest.mm b/chrome/browser/ui/cocoa/find_bar_text_field_unittest.mm
new file mode 100644
index 0000000..e9cab87
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_bar_text_field_unittest.mm
@@ -0,0 +1,92 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/find_bar_text_field.h"
+#import "chrome/browser/ui/cocoa/find_bar_text_field_cell.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+
+// OCMock wants to mock a concrete class or protocol. This should
+// provide a correct protocol for newer versions of the SDK, while
+// providing something mockable for older versions.
+
+@protocol MockTextEditingDelegate<NSControlTextEditingDelegate>
+- (void)controlTextDidBeginEditing:(NSNotification*)aNotification;
+- (BOOL)control:(NSControl*)control textShouldEndEditing:(NSText*)fieldEditor;
+@end
+
+namespace {
+
+// Width of the field so that we don't have to ask |field_| for it all
+// the time.
+static const CGFloat kWidth(300.0);
+
+class FindBarTextFieldTest : public CocoaTest {
+ public:
+ FindBarTextFieldTest() {
+ // Make sure this is wide enough to play games with the cell
+ // decorations.
+ NSRect frame = NSMakeRect(0, 0, kWidth, 30);
+ scoped_nsobject<FindBarTextField> field(
+ [[FindBarTextField alloc] initWithFrame:frame]);
+ field_ = field.get();
+
+ [field_ setStringValue:@"Test test"];
+ [[test_window() contentView] addSubview:field_];
+ }
+
+ FindBarTextField* field_;
+};
+
+// Basic view tests (AddRemove, Display).
+TEST_VIEW(FindBarTextFieldTest, field_);
+
+// Test that we have the right cell class.
+TEST_F(FindBarTextFieldTest, CellClass) {
+ EXPECT_TRUE([[field_ cell] isKindOfClass:[FindBarTextFieldCell class]]);
+}
+
+// Test that we get the same cell from -cell and
+// -findBarTextFieldCell.
+TEST_F(FindBarTextFieldTest, Cell) {
+ FindBarTextFieldCell* cell = [field_ findBarTextFieldCell];
+ EXPECT_EQ(cell, [field_ cell]);
+ EXPECT_TRUE(cell != nil);
+}
+
+// Test that becoming first responder sets things up correctly.
+TEST_F(FindBarTextFieldTest, FirstResponder) {
+ EXPECT_EQ(nil, [field_ currentEditor]);
+ EXPECT_EQ([[field_ subviews] count], 0U);
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ EXPECT_FALSE(nil == [field_ currentEditor]);
+ EXPECT_EQ([[field_ subviews] count], 1U);
+ EXPECT_TRUE([[field_ currentEditor] isDescendantOf:field_]);
+}
+
+// Test drawing, mostly to ensure nothing leaks or crashes.
+TEST_F(FindBarTextFieldTest, Display) {
+ [field_ display];
+
+ // Test focussed drawing.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ [field_ display];
+ [test_window() clearPretendKeyWindowAndFirstResponder];
+
+ // Test display of various cell configurations.
+ FindBarTextFieldCell* cell = [field_ findBarTextFieldCell];
+ [cell setActiveMatch:4 of:5];
+ [field_ display];
+
+ [cell clearResults];
+ [field_ display];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/find_bar_view.h b/chrome/browser/ui/cocoa/find_bar_view.h
new file mode 100644
index 0000000..ff5753d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_bar_view.h
@@ -0,0 +1,19 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_FIND_BAR_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_FIND_BAR_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/ui/cocoa/background_gradient_view.h"
+
+// A view that handles painting the border for the FindBar.
+
+@interface FindBarView : BackgroundGradientView {
+}
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_FIND_BAR_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/find_bar_view.mm b/chrome/browser/ui/cocoa/find_bar_view.mm
new file mode 100644
index 0000000..5e1ce21
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_bar_view.mm
@@ -0,0 +1,131 @@
+// 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/ui/cocoa/find_bar_view.h"
+
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#import "chrome/browser/ui/cocoa/url_drop_target.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+
+namespace {
+CGFloat kCurveSize = 8;
+} // end namespace
+
+@implementation FindBarView
+
+- (void)awakeFromNib {
+ // Register for all the drag types handled by the RWHVCocoa.
+ [self registerForDraggedTypes:[URLDropTargetHandler handledDragTypes]];
+}
+
+- (void)drawRect:(NSRect)rect {
+ // TODO(rohitrao): Make this prettier.
+ rect = NSInsetRect([self bounds], 0.5, 0.5);
+ rect = NSOffsetRect(rect, 0, 1.0);
+
+ NSPoint topLeft = NSMakePoint(NSMinX(rect), NSMaxY(rect));
+ NSPoint topRight = NSMakePoint(NSMaxX(rect), NSMaxY(rect));
+ NSPoint midLeft1 =
+ NSMakePoint(NSMinX(rect) + kCurveSize, NSMaxY(rect) - kCurveSize);
+ NSPoint midLeft2 =
+ NSMakePoint(NSMinX(rect) + kCurveSize, NSMinY(rect) + kCurveSize);
+ NSPoint midRight1 =
+ NSMakePoint(NSMaxX(rect) - kCurveSize, NSMinY(rect) + kCurveSize);
+ NSPoint midRight2 =
+ NSMakePoint(NSMaxX(rect) - kCurveSize, NSMaxY(rect) - kCurveSize);
+ NSPoint bottomLeft =
+ NSMakePoint(NSMinX(rect) + (2 * kCurveSize), NSMinY(rect));
+ NSPoint bottomRight =
+ NSMakePoint(NSMaxX(rect) - (2 * kCurveSize), NSMinY(rect));
+
+ NSBezierPath* path = [NSBezierPath bezierPath];
+ [path moveToPoint:topLeft];
+ [path curveToPoint:midLeft1
+ controlPoint1:NSMakePoint(midLeft1.x, topLeft.y)
+ controlPoint2:NSMakePoint(midLeft1.x, topLeft.y)];
+ [path lineToPoint:midLeft2];
+ [path curveToPoint:bottomLeft
+ controlPoint1:NSMakePoint(midLeft2.x, bottomLeft.y)
+ controlPoint2:NSMakePoint(midLeft2.x, bottomLeft.y)];
+
+ [path lineToPoint:bottomRight];
+ [path curveToPoint:midRight1
+ controlPoint1:NSMakePoint(midRight1.x, bottomLeft.y)
+ controlPoint2:NSMakePoint(midRight1.x, bottomLeft.y)];
+ [path lineToPoint:midRight2];
+ [path curveToPoint:topRight
+ controlPoint1:NSMakePoint(midRight2.x, topLeft.y)
+ controlPoint2:NSMakePoint(midRight2.x, topLeft.y)];
+ NSGraphicsContext* context = [NSGraphicsContext currentContext];
+ [context saveGraphicsState];
+ [path addClip];
+
+ // Set the pattern phase
+ NSPoint phase = [[self window] themePatternPhase];
+
+ [context setPatternPhase:phase];
+ [super drawBackground];
+ [context restoreGraphicsState];
+
+ [[self strokeColor] set];
+ [path stroke];
+}
+
+// The findbar is mostly opaque, but has an 8px transparent border on the left
+// and right sides (see |kCurveSize|). This is an artifact of the way it is
+// drawn. We override hitTest to return nil for points in this transparent
+// area.
+- (NSView*)hitTest:(NSPoint)point {
+ NSView* hitView = [super hitTest:point];
+ if (hitView == self) {
+ // |rect| is approximately equivalent to the opaque area of the findbar.
+ NSRect rect = NSInsetRect([self bounds], kCurveSize, 0);
+ if (!NSMouseInRect(point, rect, [self isFlipped]))
+ return nil;
+ }
+
+ return hitView;
+}
+
+// Eat all mouse events, to prevent clicks from falling through to views below.
+- (void)mouseDown:(NSEvent *)theEvent {
+}
+
+- (void)rightMouseDown:(NSEvent *)theEvent {
+}
+
+- (void)otherMouseDown:(NSEvent *)theEvent {
+}
+
+- (void)mouseUp:(NSEvent *)theEvent {
+}
+
+- (void)rightMouseUp:(NSEvent *)theEvent {
+}
+
+- (void)otherMouseUp:(NSEvent *)theEvent {
+}
+
+- (void)mouseMoved:(NSEvent *)theEvent {
+}
+
+- (void)mouseDragged:(NSEvent *)theEvent {
+}
+
+- (void)rightMouseDragged:(NSEvent *)theEvent {
+}
+
+- (void)otherMouseDragged:(NSEvent *)theEvent {
+}
+
+// Eat drag operations, to prevent drags from going through to the views below.
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info {
+ return NSDragOperationNone;
+}
+
+- (ViewID)viewID {
+ return VIEW_ID_FIND_IN_PAGE;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/find_bar_view_unittest.mm b/chrome/browser/ui/cocoa/find_bar_view_unittest.mm
new file mode 100644
index 0000000..639b5ef
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_bar_view_unittest.mm
@@ -0,0 +1,90 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/find_bar_view.h"
+#include "chrome/browser/ui/cocoa/test_event_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface MouseDownViewPong : NSView {
+ BOOL pong_;
+}
+@property (nonatomic, assign) BOOL pong;
+@end
+
+@implementation MouseDownViewPong
+@synthesize pong = pong_;
+- (void)mouseDown:(NSEvent*)event {
+ pong_ = YES;
+}
+@end
+
+
+namespace {
+
+class FindBarViewTest : public CocoaTest {
+ public:
+ FindBarViewTest() {
+ NSRect frame = NSMakeRect(0, 0, 100, 30);
+ scoped_nsobject<FindBarView> view(
+ [[FindBarView alloc] initWithFrame:frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ FindBarView* view_;
+};
+
+TEST_VIEW(FindBarViewTest, view_)
+
+TEST_F(FindBarViewTest, FindBarEatsMouseClicksInBackgroundArea) {
+ scoped_nsobject<MouseDownViewPong> pongView(
+ [[MouseDownViewPong alloc] initWithFrame:NSMakeRect(0, 0, 200, 200)]);
+
+ // Remove all of the subviews of the findbar, to make sure we don't
+ // accidentally hit a subview when trying to simulate a click in the
+ // background area.
+ [view_ setSubviews:[NSArray array]];
+ [view_ setFrame:NSMakeRect(0, 0, 200, 200)];
+
+ // Add the pong view as a sibling of the findbar.
+ [[test_window() contentView] addSubview:pongView.get()
+ positioned:NSWindowBelow
+ relativeTo:view_];
+
+ // Synthesize a mousedown event and send it to the window. The event is
+ // placed in the center of the find bar.
+ NSPoint pointInCenterOfFindBar = NSMakePoint(100, 100);
+ [pongView setPong:NO];
+ [test_window()
+ sendEvent:test_event_utils::LeftMouseDownAtPoint(pointInCenterOfFindBar)];
+ // Click gets eaten by findbar, not passed through to underlying view.
+ EXPECT_FALSE([pongView pong]);
+}
+
+TEST_F(FindBarViewTest, FindBarPassesThroughClicksInTransparentArea) {
+ scoped_nsobject<MouseDownViewPong> pongView(
+ [[MouseDownViewPong alloc] initWithFrame:NSMakeRect(0, 0, 200, 200)]);
+ [view_ setFrame:NSMakeRect(0, 0, 200, 200)];
+
+ // Add the pong view as a sibling of the findbar.
+ [[test_window() contentView] addSubview:pongView.get()
+ positioned:NSWindowBelow
+ relativeTo:view_];
+
+ // Synthesize a mousedown event and send it to the window. The event is inset
+ // a few pixels from the lower left corner of the window, which places it in
+ // the transparent area surrounding the findbar.
+ NSPoint pointInTransparentArea = NSMakePoint(2, 2);
+ [pongView setPong:NO];
+ [test_window()
+ sendEvent:test_event_utils::LeftMouseDownAtPoint(pointInTransparentArea)];
+ // Click is ignored by findbar, passed through to underlying view.
+ EXPECT_TRUE([pongView pong]);
+}
+} // namespace
diff --git a/chrome/browser/ui/cocoa/find_pasteboard.h b/chrome/browser/ui/cocoa/find_pasteboard.h
new file mode 100644
index 0000000..f7153f6
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_pasteboard.h
@@ -0,0 +1,58 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_FIND_PASTEBOARD_H_
+#define CHROME_BROWSER_UI_COCOA_FIND_PASTEBOARD_H_
+#pragma once
+
+#include "base/string16.h"
+
+#ifdef __OBJC__
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+
+extern NSString* kFindPasteboardChangedNotification;
+
+// Manages the find pasteboard. Use this to copy text to the find pasteboard,
+// to get the text currently on the find pasteboard, and to receive
+// notifications when the text on the find pasteboard has changed. You should
+// always use this class instead of accessing
+// [NSPasteboard pasteboardWithName:NSFindPboard] directly.
+//
+// This is not thread-safe and must be used on the main thread.
+//
+// This is supposed to be a singleton.
+@interface FindPasteboard : NSObject {
+ @private
+ scoped_nsobject<NSString> findText_;
+}
+
+// Returns the singleton instance of this class.
++ (FindPasteboard*)sharedInstance;
+
+// Returns the current find text. This is never nil; if there is no text on the
+// find pasteboard, this returns an empty string.
+- (NSString*)findText;
+
+// Sets the current find text to |newText| and sends a
+// |kFindPasteboardChangedNotification| to the default notification center if
+// it the new text different from the current text. |newText| must not be nil.
+- (void)setFindText:(NSString*)newText;
+@end
+
+@interface FindPasteboard (TestingAPI)
+- (void)loadTextFromPasteboard:(NSNotification*)notification;
+
+// This methods is meant to be overridden in tests.
+- (NSPasteboard*)findPboard;
+@end
+
+#endif // __OBJC__
+
+// Also provide a c++ interface
+string16 GetFindPboardText();
+
+#endif // CHROME_BROWSER_UI_COCOA_FIND_PASTEBOARD_H_
diff --git a/chrome/browser/ui/cocoa/find_pasteboard.mm b/chrome/browser/ui/cocoa/find_pasteboard.mm
new file mode 100644
index 0000000..0e86111
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_pasteboard.mm
@@ -0,0 +1,82 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/find_pasteboard.h"
+
+#include "base/logging.h"
+#include "base/sys_string_conversions.h"
+
+NSString* kFindPasteboardChangedNotification =
+ @"kFindPasteboardChangedNotification_Chrome";
+
+@implementation FindPasteboard
+
++ (FindPasteboard*)sharedInstance {
+ static FindPasteboard* instance = nil;
+ if (!instance) {
+ instance = [[FindPasteboard alloc] init];
+ }
+ return instance;
+}
+
+- (id)init {
+ if ((self = [super init])) {
+ findText_.reset([[NSString alloc] init]);
+
+ // Check if the text in the findboard has changed on app activate.
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(loadTextFromPasteboard:)
+ name:NSApplicationDidBecomeActiveNotification
+ object:nil];
+ [self loadTextFromPasteboard:nil];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ // Since this is a singleton, this should only be executed in test code.
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+- (NSPasteboard*)findPboard {
+ return [NSPasteboard pasteboardWithName:NSFindPboard];
+}
+
+- (void)loadTextFromPasteboard:(NSNotification*)notification {
+ NSPasteboard* findPboard = [self findPboard];
+ if ([[findPboard types] containsObject:NSStringPboardType])
+ [self setFindText:[findPboard stringForType:NSStringPboardType]];
+}
+
+- (NSString*)findText {
+ return findText_;
+}
+
+- (void)setFindText:(NSString*)newText {
+ DCHECK(newText);
+ if (!newText)
+ return;
+
+ DCHECK([NSThread isMainThread]);
+
+ BOOL needToSendNotification = ![findText_.get() isEqualToString:newText];
+ if (needToSendNotification) {
+ findText_.reset([newText copy]);
+ NSPasteboard* findPboard = [self findPboard];
+ [findPboard declareTypes:[NSArray arrayWithObject:NSStringPboardType]
+ owner:nil];
+ [findPboard setString:findText_.get() forType:NSStringPboardType];
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kFindPasteboardChangedNotification
+ object:self];
+ }
+}
+
+@end
+
+string16 GetFindPboardText() {
+ return base::SysNSStringToUTF16([[FindPasteboard sharedInstance] findText]);
+}
diff --git a/chrome/browser/ui/cocoa/find_pasteboard_unittest.mm b/chrome/browser/ui/cocoa/find_pasteboard_unittest.mm
new file mode 100644
index 0000000..739eff3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/find_pasteboard_unittest.mm
@@ -0,0 +1,115 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/find_pasteboard.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+// A subclass of FindPasteboard that doesn't write to the real find pasteboard.
+@interface FindPasteboardTesting : FindPasteboard {
+ @public
+ int notificationCount_;
+ @private
+ NSPasteboard* pboard_;
+}
+- (NSPasteboard*)findPboard;
+
+- (void)callback:(id)sender;
+
+// These are for checking that pasteboard content is copied to/from the
+// FindPasteboard correctly.
+- (NSString*)findPboardText;
+- (void)setFindPboardText:(NSString*)text;
+@end
+
+@implementation FindPasteboardTesting
+
+- (id)init {
+ if ((self = [super init])) {
+ pboard_ = [NSPasteboard pasteboardWithUniqueName];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [pboard_ releaseGlobally];
+ [super dealloc];
+}
+
+- (NSPasteboard*)findPboard {
+ return pboard_;
+}
+
+- (void)callback:(id)sender {
+ ++notificationCount_;
+}
+
+- (void)setFindPboardText:(NSString*)text {
+ [pboard_ declareTypes:[NSArray arrayWithObject:NSStringPboardType]
+ owner:nil];
+ [pboard_ setString:text forType:NSStringPboardType];
+}
+
+- (NSString*)findPboardText {
+ return [pboard_ stringForType:NSStringPboardType];
+}
+@end
+
+namespace {
+
+class FindPasteboardTest : public CocoaTest {
+ public:
+ FindPasteboardTest() {
+ pboard_.reset([[FindPasteboardTesting alloc] init]);
+ }
+ protected:
+ scoped_nsobject<FindPasteboardTesting> pboard_;
+};
+
+TEST_F(FindPasteboardTest, SettingTextUpdatesPboard) {
+ [pboard_.get() setFindText:@"text"];
+ EXPECT_EQ(
+ NSOrderedSame,
+ [[pboard_.get() findPboardText] compare:@"text"]);
+}
+
+TEST_F(FindPasteboardTest, ReadingFromPboardUpdatesFindText) {
+ [pboard_.get() setFindPboardText:@"text"];
+ [pboard_.get() loadTextFromPasteboard:nil];
+ EXPECT_EQ(
+ NSOrderedSame,
+ [[pboard_.get() findText] compare:@"text"]);
+}
+
+TEST_F(FindPasteboardTest, SendsNotificationWhenTextChanges) {
+ [[NSNotificationCenter defaultCenter]
+ addObserver:pboard_.get()
+ selector:@selector(callback:)
+ name:kFindPasteboardChangedNotification
+ object:pboard_.get()];
+ EXPECT_EQ(0, pboard_.get()->notificationCount_);
+ [pboard_.get() setFindText:@"text"];
+ EXPECT_EQ(1, pboard_.get()->notificationCount_);
+ [pboard_.get() setFindText:@"text"];
+ EXPECT_EQ(1, pboard_.get()->notificationCount_);
+ [pboard_.get() setFindText:@"other text"];
+ EXPECT_EQ(2, pboard_.get()->notificationCount_);
+
+ [pboard_.get() setFindPboardText:@"other text"];
+ [pboard_.get() loadTextFromPasteboard:nil];
+ EXPECT_EQ(2, pboard_.get()->notificationCount_);
+
+ [pboard_.get() setFindPboardText:@"otherer text"];
+ [pboard_.get() loadTextFromPasteboard:nil];
+ EXPECT_EQ(3, pboard_.get()->notificationCount_);
+
+ [[NSNotificationCenter defaultCenter] removeObserver:pboard_.get()];
+}
+
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/first_run_bubble_controller.h b/chrome/browser/ui/cocoa/first_run_bubble_controller.h
new file mode 100644
index 0000000..5c5f768
--- /dev/null
+++ b/chrome/browser/ui/cocoa/first_run_bubble_controller.h
@@ -0,0 +1,24 @@
+// 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/ui/cocoa/base_bubble_controller.h"
+
+class Profile;
+
+// Manages the first run bubble.
+@interface FirstRunBubbleController : BaseBubbleController {
+ @private
+ // Header label.
+ IBOutlet NSTextField* header_;
+
+ Profile* profile_;
+}
+
+// Creates and shows a firstRun bubble.
++ (FirstRunBubbleController*) showForView:(NSView*)view
+ offset:(NSPoint)offset
+ profile:(Profile*)profile;
+@end
diff --git a/chrome/browser/ui/cocoa/first_run_bubble_controller.mm b/chrome/browser/ui/cocoa/first_run_bubble_controller.mm
new file mode 100644
index 0000000..a61239a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/first_run_bubble_controller.mm
@@ -0,0 +1,84 @@
+// 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/ui/cocoa/first_run_bubble_controller.h"
+
+#include "app/l10n_util.h"
+#include "base/logging.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/search_engines/util.h"
+#import "chrome/browser/ui/cocoa/l10n_util.h"
+#import "chrome/browser/ui/cocoa/info_bubble_view.h"
+#include "grit/generated_resources.h"
+
+@interface FirstRunBubbleController(Private)
+- (id)initRelativeToView:(NSView*)view
+ offset:(NSPoint)offset
+ profile:(Profile*)profile;
+- (void)closeIfNotKey;
+@end
+
+@implementation FirstRunBubbleController
+
++ (FirstRunBubbleController*) showForView:(NSView*)view
+ offset:(NSPoint)offset
+ profile:(Profile*)profile {
+ // Autoreleases itself on bubble close.
+ return [[FirstRunBubbleController alloc] initRelativeToView:view
+ offset:offset
+ profile:profile];
+}
+
+- (id)initRelativeToView:(NSView*)view
+ offset:(NSPoint)offset
+ profile:(Profile*)profile {
+ if ((self = [super initWithWindowNibPath:@"FirstRunBubble"
+ relativeToView:view
+ offset:offset])) {
+ profile_ = profile;
+ [self showWindow:nil];
+
+ // On 10.5, the first run bubble sometimes does not disappear when clicking
+ // the omnibox. This happens if the bubble never became key, due to it
+ // showing up so early in the startup sequence. As a workaround, close it
+ // automatically after a few seconds if it doesn't become key.
+ // http://crbug.com/52726
+ [self performSelector:@selector(closeIfNotKey) withObject:nil afterDelay:3];
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ [[self bubble] setBubbleType:info_bubble::kWhiteInfoBubble];
+
+ DCHECK(header_);
+ [header_ setStringValue:cocoa_l10n_util::ReplaceNSStringPlaceholders(
+ [header_ stringValue], GetDefaultSearchEngineName(profile_), NULL)];
+
+ // Adapt window size to bottom buttons. Do this before all other layouting.
+ CGFloat dy = cocoa_l10n_util::VerticallyReflowGroup([[self bubble] subviews]);
+ NSSize ds = NSMakeSize(0, dy);
+ ds = [[self bubble] convertSize:ds toView:nil];
+
+ NSRect frame = [[self window] frame];
+ frame.origin.y -= ds.height;
+ frame.size.height += ds.height;
+ [[self window] setFrame:frame display:YES];
+}
+
+- (void)close {
+ // If the window is closed before the timer is fired, cancel the timer, since
+ // it retains the controller.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(closeIfNotKey)
+ object:nil];
+ [super close];
+}
+
+- (void)closeIfNotKey {
+ if (![[self window] isKeyWindow])
+ [self close];
+}
+
+@end // FirstRunBubbleController
diff --git a/chrome/browser/ui/cocoa/first_run_bubble_controller_unittest.mm b/chrome/browser/ui/cocoa/first_run_bubble_controller_unittest.mm
new file mode 100644
index 0000000..d9f37c93
--- /dev/null
+++ b/chrome/browser/ui/cocoa/first_run_bubble_controller_unittest.mm
@@ -0,0 +1,44 @@
+// 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/ui/cocoa/first_run_bubble_controller.h"
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/debug/debugger.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+class FirstRunBubbleControllerTest : public CocoaTest {
+ public:
+ BrowserTestHelper helper_;
+};
+
+// Check that the bubble doesn't crash or leak.
+TEST_F(FirstRunBubbleControllerTest, Init) {
+ scoped_nsobject<NSWindow> parent([[NSWindow alloc]
+ initWithContentRect:NSMakeRect(0, 0, 800, 600)
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:NO]);
+ [parent setReleasedWhenClosed:NO];
+ if (base::debug::BeingDebugged())
+ [parent.get() orderFront:nil];
+ else
+ [parent.get() orderBack:nil];
+
+ FirstRunBubbleController* controller = [FirstRunBubbleController
+ showForView:[parent.get() contentView]
+ offset:NSMakePoint(300, 300)
+ profile:helper_.profile()];
+ EXPECT_TRUE(controller != nil);
+ EXPECT_TRUE([[controller window] isVisible]);
+ [parent.get() close];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/first_run_dialog.h b/chrome/browser/ui/cocoa/first_run_dialog.h
new file mode 100644
index 0000000..3e575a3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/first_run_dialog.h
@@ -0,0 +1,36 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_FIRST_RUN_DIALOG_H_
+#define CHROME_BROWSER_FIRST_RUN_DIALOG_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// Class that acts as a controller for the modal first run dialog.
+// The dialog asks the user's explicit permission for reporting stats to help
+// us improve Chromium.
+@interface FirstRunDialogController : NSWindowController {
+ @private
+ BOOL statsEnabled_;
+ BOOL makeDefaultBrowser_;
+
+ IBOutlet NSArray* objectsToSize_;
+ IBOutlet NSButton* statsCheckbox_;
+ BOOL beenSized_;
+}
+
+// Called when the "Start Google Chrome" button is pressed.
+- (IBAction)ok:(id)sender;
+
+// Called when the "Learn More" button is pressed.
+- (IBAction)learnMore:(id)sender;
+
+// Properties for bindings.
+@property(assign, nonatomic) BOOL statsEnabled;
+@property(assign, nonatomic) BOOL makeDefaultBrowser;
+
+@end
+
+#endif // CHROME_BROWSER_FIRST_RUN_DIALOG_H_
diff --git a/chrome/browser/ui/cocoa/first_run_dialog.mm b/chrome/browser/ui/cocoa/first_run_dialog.mm
new file mode 100644
index 0000000..9590b9b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/first_run_dialog.mm
@@ -0,0 +1,195 @@
+// 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/ui/cocoa/first_run_dialog.h"
+
+#include "app/l10n_util_mac.h"
+#include "base/mac_util.h"
+#include "base/message_loop.h"
+#include "base/ref_counted.h"
+#include "grit/locale_settings.h"
+#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+@interface FirstRunDialogController (PrivateMethods)
+// Show the dialog.
+- (void)show;
+@end
+
+namespace {
+
+// Compare function for -[NSArray sortedArrayUsingFunction:context:] that
+// sorts the views in Y order bottom up.
+NSInteger CompareFrameY(id view1, id view2, void* context) {
+ CGFloat y1 = NSMinY([view1 frame]);
+ CGFloat y2 = NSMinY([view2 frame]);
+ if (y1 < y2)
+ return NSOrderedAscending;
+ else if (y1 > y2)
+ return NSOrderedDescending;
+ else
+ return NSOrderedSame;
+}
+
+class FirstRunShowBridge : public base::RefCounted<FirstRunShowBridge> {
+ public:
+ FirstRunShowBridge(FirstRunDialogController* controller);
+
+ void ShowDialog();
+ private:
+ FirstRunDialogController* controller_;
+};
+
+FirstRunShowBridge::FirstRunShowBridge(
+ FirstRunDialogController* controller) : controller_(controller) {
+}
+
+void FirstRunShowBridge::ShowDialog() {
+ [controller_ show];
+ MessageLoop::current()->QuitNow();
+}
+
+};
+
+@implementation FirstRunDialogController
+
+@synthesize statsEnabled = statsEnabled_;
+@synthesize makeDefaultBrowser = makeDefaultBrowser_;
+
+- (id)init {
+ NSString* nibpath =
+ [mac_util::MainAppBundle() pathForResource:@"FirstRunDialog"
+ ofType:@"nib"];
+ self = [super initWithWindowNibPath:nibpath owner:self];
+ if (self != nil) {
+ // Bound to the dialog checkbox, default to true.
+ makeDefaultBrowser_ = YES;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [super dealloc];
+}
+
+- (IBAction)showWindow:(id)sender {
+ // The main MessageLoop has not yet run, but has been spun. If we call
+ // -[NSApplication runModalForWindow:] we will hang <http://crbug.com/54248>.
+ // Therefore the main MessageLoop is run so things work.
+
+ scoped_refptr<FirstRunShowBridge> bridge(new FirstRunShowBridge(self));
+ MessageLoop::current()->PostTask(
+ FROM_HERE,
+ NewRunnableMethod(bridge.get(),
+ &FirstRunShowBridge::ShowDialog));
+ MessageLoop::current()->Run();
+}
+
+- (void)show {
+ NSWindow* win = [self window];
+
+ // Only support the sizing the window once.
+ DCHECK(!beenSized_) << "ShowWindow was called twice?";
+ if (!beenSized_) {
+ beenSized_ = YES;
+ DCHECK_GT([objectsToSize_ count], 0U);
+
+ // Size everything to fit, collecting the widest growth needed (XIB provides
+ // the min size, i.e.-never shrink, just grow).
+ CGFloat largestWidthChange = 0.0;
+ for (NSView* view in objectsToSize_) {
+ DCHECK_NE(statsCheckbox_, view) << "Stats checkbox shouldn't be in list";
+ if (![view isHidden]) {
+ NSSize delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:view];
+ DCHECK_EQ(delta.height, 0.0)
+ << "Didn't expect anything to change heights";
+ if (largestWidthChange < delta.width)
+ largestWidthChange = delta.width;
+ }
+ }
+
+ // Make the window wide enough to fit everything.
+ if (largestWidthChange > 0.0) {
+ NSView* contentView = [win contentView];
+ NSRect windowFrame = [contentView convertRect:[win frame] fromView:nil];
+ windowFrame.size.width += largestWidthChange;
+ windowFrame = [contentView convertRect:windowFrame toView:nil];
+ [win setFrame:windowFrame display:NO];
+ }
+
+ // The stats checkbox gets some really long text, so it gets word wrapped
+ // and then sized.
+ DCHECK(statsCheckbox_);
+ CGFloat statsCheckboxHeightChange = 0.0;
+ [GTMUILocalizerAndLayoutTweaker wrapButtonTitleForWidth:statsCheckbox_];
+ statsCheckboxHeightChange =
+ [GTMUILocalizerAndLayoutTweaker sizeToFitView:statsCheckbox_].height;
+
+ // Walk bottom up shuffling for all the hidden views.
+ NSArray* subViews =
+ [[[win contentView] subviews] sortedArrayUsingFunction:CompareFrameY
+ context:NULL];
+ CGFloat moveDown = 0.0;
+ NSUInteger numSubViews = [subViews count];
+ for (NSUInteger idx = 0 ; idx < numSubViews ; ++idx) {
+ NSView* view = [subViews objectAtIndex:idx];
+
+ // If the view is hidden, collect the amount to move everything above it
+ // down, if it's not hidden, apply any shift down.
+ if ([view isHidden]) {
+ DCHECK_GT((numSubViews - 1), idx)
+ << "Don't support top view being hidden";
+ NSView* nextView = [subViews objectAtIndex:(idx + 1)];
+ CGFloat viewBottom = [view frame].origin.y;
+ CGFloat nextViewBottom = [nextView frame].origin.y;
+ moveDown += nextViewBottom - viewBottom;
+ } else {
+ if (moveDown != 0.0) {
+ NSPoint origin = [view frame].origin;
+ origin.y -= moveDown;
+ [view setFrameOrigin:origin];
+ }
+ }
+ // Special case, if this is the stats checkbox, everything above it needs
+ // to get moved up by the amount it changed height.
+ if (view == statsCheckbox_) {
+ moveDown -= statsCheckboxHeightChange;
+ }
+ }
+
+ // Resize the window for any height change from hidden views, etc.
+ if (moveDown != 0.0) {
+ NSView* contentView = [win contentView];
+ [contentView setAutoresizesSubviews:NO];
+ NSRect windowFrame = [contentView convertRect:[win frame] fromView:nil];
+ windowFrame.size.height -= moveDown;
+ windowFrame = [contentView convertRect:windowFrame toView:nil];
+ [win setFrame:windowFrame display:NO];
+ [contentView setAutoresizesSubviews:YES];
+ }
+
+ }
+
+ // Neat weirdness in the below code - the Application menu stays enabled
+ // while the window is open but selecting items from it (e.g. Quit) has
+ // no effect. I'm guessing that this is an artifact of us being a
+ // background-only application at this stage and displaying a modal
+ // window.
+
+ // Display dialog.
+ [win center];
+ [NSApp runModalForWindow:win];
+}
+
+- (IBAction)ok:(id)sender {
+ [[self window] close];
+ [NSApp stopModal];
+}
+
+- (IBAction)learnMore:(id)sender {
+ NSString* urlStr = l10n_util::GetNSString(IDS_LEARN_MORE_REPORTING_URL);
+ NSURL* learnMoreUrl = [NSURL URLWithString:urlStr];
+ [[NSWorkspace sharedWorkspace] openURL:learnMoreUrl];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/floating_bar_backing_view.h b/chrome/browser/ui/cocoa/floating_bar_backing_view.h
new file mode 100644
index 0000000..7cd6b2d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/floating_bar_backing_view.h
@@ -0,0 +1,15 @@
+// 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_UI_COCOA_FLOATING_BAR_BACKING_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_FLOATING_BAR_BACKING_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// A custom view that draws the tab strip background for fullscreen windows.
+@interface FloatingBarBackingView : NSView
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_FLOATING_BAR_BACKING_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/floating_bar_backing_view.mm b/chrome/browser/ui/cocoa/floating_bar_backing_view.mm
new file mode 100644
index 0000000..70f785c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/floating_bar_backing_view.mm
@@ -0,0 +1,51 @@
+// 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 "chrome/browser/ui/cocoa/floating_bar_backing_view.h"
+
+#include "base/mac_util.h"
+#import "chrome/browser/ui/cocoa/browser_frame_view.h"
+
+@implementation FloatingBarBackingView
+
+- (void)drawRect:(NSRect)rect {
+ NSWindow* window = [self window];
+ BOOL isMainWindow = [window isMainWindow];
+
+ if (isMainWindow)
+ [[NSColor windowFrameColor] set];
+ else
+ [[NSColor windowBackgroundColor] set];
+ NSRectFill(rect);
+
+ // TODO(rohitrao): Don't assume -22 here.
+ [BrowserFrameView drawWindowThemeInDirtyRect:rect
+ forView:self
+ bounds:[self bounds]
+ offset:NSMakePoint(0, -22)
+ forceBlackBackground:YES];
+
+}
+
+// Eat all mouse events (and do *not* pass them on to the next responder!).
+- (void)mouseDown:(NSEvent*)event {}
+- (void)rightMouseDown:(NSEvent*)event {}
+- (void)otherMouseDown:(NSEvent*)event {}
+- (void)rightMouseUp:(NSEvent*)event {}
+- (void)otherMouseUp:(NSEvent*)event {}
+- (void)mouseMoved:(NSEvent*)event {}
+- (void)mouseDragged:(NSEvent*)event {}
+- (void)rightMouseDragged:(NSEvent*)event {}
+- (void)otherMouseDragged:(NSEvent*)event {}
+
+// Eat this too, except that ...
+- (void)mouseUp:(NSEvent*)event {
+ // a double-click in the blank area should try to minimize, to be consistent
+ // with double-clicks on the contiguous tab strip area. (It'll fail and beep.)
+ if ([event clickCount] == 2 &&
+ mac_util::ShouldWindowsMiniaturizeOnDoubleClick())
+ [[self window] performMiniaturize:self];
+}
+
+@end // @implementation FloatingBarBackingView
diff --git a/chrome/browser/ui/cocoa/floating_bar_backing_view_unittest.mm b/chrome/browser/ui/cocoa/floating_bar_backing_view_unittest.mm
new file mode 100644
index 0000000..4753a26
--- /dev/null
+++ b/chrome/browser/ui/cocoa/floating_bar_backing_view_unittest.mm
@@ -0,0 +1,26 @@
+// 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/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/floating_bar_backing_view.h"
+
+namespace {
+
+class FloatingBarBackingViewTest : public CocoaTest {
+ public:
+ FloatingBarBackingViewTest() {
+ NSRect content_frame = [[test_window() contentView] frame];
+ scoped_nsobject<FloatingBarBackingView> view(
+ [[FloatingBarBackingView alloc] initWithFrame:content_frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ FloatingBarBackingView* view_; // Weak. Owned by the view hierarchy.
+};
+
+// Tests display, add/remove.
+TEST_VIEW(FloatingBarBackingViewTest, view_);
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/focus_tracker.h b/chrome/browser/ui/cocoa/focus_tracker.h
new file mode 100644
index 0000000..f828979
--- /dev/null
+++ b/chrome/browser/ui/cocoa/focus_tracker.h
@@ -0,0 +1,28 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+
+// A class that handles saving and restoring focus. An instance of
+// this class snapshots the currently focused view when it is
+// constructed, and callers can use restoreFocus to return focus to
+// that view. FocusTracker will not restore focus to views that are
+// no longer in the view hierarchy or are not in the correct window.
+
+@interface FocusTracker : NSObject {
+ @private
+ scoped_nsobject<NSView> focusedView_;
+}
+
+// |window| is the window that we are saving focus for. This
+// method snapshots the currently focused view.
+- (id)initWithWindow:(NSWindow*)window;
+
+// Attempts to restore focus to the snapshotted view. Returns YES if
+// focus was restored. Will not restore focus if the view is no
+// longer in the view hierarchy under |window|.
+- (BOOL)restoreFocusInWindow:(NSWindow*)window;
+@end
diff --git a/chrome/browser/ui/cocoa/focus_tracker.mm b/chrome/browser/ui/cocoa/focus_tracker.mm
new file mode 100644
index 0000000..dc52791
--- /dev/null
+++ b/chrome/browser/ui/cocoa/focus_tracker.mm
@@ -0,0 +1,46 @@
+// 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/ui/cocoa/focus_tracker.h"
+
+#include "base/basictypes.h"
+
+@implementation FocusTracker
+
+- (id)initWithWindow:(NSWindow*)window {
+ if ((self = [super init])) {
+ NSResponder* current_focus = [window firstResponder];
+
+ // Special case NSTextViews, because they are removed from the
+ // view hierarchy when their text field does not have focus. If
+ // an NSTextView is the current first responder, save a pointer to
+ // its NSTextField delegate instead.
+ if ([current_focus isKindOfClass:[NSTextView class]]) {
+ id delegate = [(NSTextView*)current_focus delegate];
+ if ([delegate isKindOfClass:[NSTextField class]])
+ current_focus = delegate;
+ else
+ current_focus = nil;
+ }
+
+ if ([current_focus isKindOfClass:[NSView class]]) {
+ NSView* current_focus_view = (NSView*)current_focus;
+ focusedView_.reset([current_focus_view retain]);
+ }
+ }
+
+ return self;
+}
+
+- (BOOL)restoreFocusInWindow:(NSWindow*)window {
+ if (!focusedView_.get())
+ return NO;
+
+ if ([focusedView_ window] && [focusedView_ window] == window)
+ return [window makeFirstResponder:focusedView_.get()];
+
+ return NO;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/focus_tracker_unittest.mm b/chrome/browser/ui/cocoa/focus_tracker_unittest.mm
new file mode 100644
index 0000000..9868cbd
--- /dev/null
+++ b/chrome/browser/ui/cocoa/focus_tracker_unittest.mm
@@ -0,0 +1,90 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/focus_tracker.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class FocusTrackerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ scoped_nsobject<NSView> view([[NSView alloc] initWithFrame:NSZeroRect]);
+ viewA_ = view.get();
+ [[test_window() contentView] addSubview:viewA_];
+
+ view.reset([[NSView alloc] initWithFrame:NSZeroRect]);
+ viewB_ = view.get();
+ [[test_window() contentView] addSubview:viewB_];
+ }
+
+ protected:
+ NSView* viewA_;
+ NSView* viewB_;
+};
+
+TEST_F(FocusTrackerTest, SaveRestore) {
+ NSWindow* window = test_window();
+ ASSERT_TRUE([window makeFirstResponder:viewA_]);
+ scoped_nsobject<FocusTracker> tracker(
+ [[FocusTracker alloc] initWithWindow:window]);
+ // Give focus to |viewB_|, then try and restore it to view1.
+ ASSERT_TRUE([window makeFirstResponder:viewB_]);
+ EXPECT_TRUE([tracker restoreFocusInWindow:window]);
+ EXPECT_EQ(viewA_, [window firstResponder]);
+}
+
+TEST_F(FocusTrackerTest, SaveRestoreWithTextView) {
+ // Valgrind will complain if the text field has zero size.
+ NSRect frame = NSMakeRect(0, 0, 100, 20);
+ NSWindow* window = test_window();
+ scoped_nsobject<NSTextField> text([[NSTextField alloc] initWithFrame:frame]);
+ [[window contentView] addSubview:text];
+
+ ASSERT_TRUE([window makeFirstResponder:text]);
+ scoped_nsobject<FocusTracker> tracker([[FocusTracker alloc]
+ initWithWindow:window]);
+ // Give focus to |viewB_|, then try and restore it to the text field.
+ ASSERT_TRUE([window makeFirstResponder:viewB_]);
+ EXPECT_TRUE([tracker restoreFocusInWindow:window]);
+ EXPECT_TRUE([[window firstResponder] isKindOfClass:[NSTextView class]]);
+}
+
+TEST_F(FocusTrackerTest, DontRestoreToViewNotInWindow) {
+ NSWindow* window = test_window();
+ scoped_nsobject<NSView> viewC([[NSView alloc] initWithFrame:NSZeroRect]);
+ [[window contentView] addSubview:viewC];
+
+ ASSERT_TRUE([window makeFirstResponder:viewC]);
+ scoped_nsobject<FocusTracker> tracker(
+ [[FocusTracker alloc] initWithWindow:window]);
+
+ // Give focus to |viewB_|, then remove viewC from the hierarchy and try
+ // to restore focus. The restore should fail.
+ ASSERT_TRUE([window makeFirstResponder:viewB_]);
+ [viewC removeFromSuperview];
+ EXPECT_FALSE([tracker restoreFocusInWindow:window]);
+}
+
+TEST_F(FocusTrackerTest, DontRestoreFocusToViewInDifferentWindow) {
+ NSWindow* window = test_window();
+ ASSERT_TRUE([window makeFirstResponder:viewA_]);
+ scoped_nsobject<FocusTracker> tracker(
+ [[FocusTracker alloc] initWithWindow:window]);
+
+ // Give focus to |viewB_|, then try and restore focus in a different
+ // window. It is ok to pass a nil NSWindow here because we only use
+ // it for direct comparison.
+ ASSERT_TRUE([window makeFirstResponder:viewB_]);
+ EXPECT_FALSE([tracker restoreFocusInWindow:nil]);
+}
+
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/font_language_settings_controller.h b/chrome/browser/ui/cocoa/font_language_settings_controller.h
new file mode 100644
index 0000000..9e03323
--- /dev/null
+++ b/chrome/browser/ui/cocoa/font_language_settings_controller.h
@@ -0,0 +1,94 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/prefs/pref_member.h"
+
+class Profile;
+
+// Used to keep track of which type of font the user is currently selecting.
+enum FontSettingType {
+ FontSettingSerif,
+ FontSettingSansSerif,
+ FontSettingFixed
+};
+
+// Keys for the dictionaries in the |encodings_| array.
+extern NSString* const kCharacterInfoEncoding; // NSString value.
+extern NSString* const kCharacterInfoName; // NSString value.
+extern NSString* const kCharacterInfoID; // NSNumber value.
+
+// A window controller that allows the user to change the default WebKit fonts
+// and language encodings for web pages. This window controller is meant to be
+// used as a modal sheet on another window.
+@interface FontLanguageSettingsController : NSWindowController
+ <NSWindowDelegate> {
+ @private
+ // The font that we are currently changing.
+ NSFont* currentFont_; // weak
+ FontSettingType currentType_;
+
+ IBOutlet NSButton* serifButton_;
+ IBOutlet NSTextField* serifField_;
+ scoped_nsobject<NSFont> serifFont_;
+ IBOutlet NSTextField* serifLabel_;
+ BOOL changedSerif_;
+
+ IBOutlet NSButton* sansSerifButton_;
+ IBOutlet NSTextField* sansSerifField_;
+ scoped_nsobject<NSFont> sansSerifFont_;
+ IBOutlet NSTextField* sansSerifLabel_;
+ BOOL changedSansSerif_;
+
+ IBOutlet NSButton* fixedWidthButton_;
+ IBOutlet NSTextField* fixedWidthField_;
+ scoped_nsobject<NSFont> fixedWidthFont_;
+ IBOutlet NSTextField* fixedWidthLabel_;
+ BOOL changedFixedWidth_;
+
+ // The actual preference members.
+ StringPrefMember serifName_;
+ StringPrefMember sansSerifName_;
+ StringPrefMember fixedWidthName_;
+ IntegerPrefMember serifSize_;
+ IntegerPrefMember sansSerifSize_;
+ IntegerPrefMember fixedWidthSize_;
+
+ // Array of dictionaries that contain the canonical encoding name, human-
+ // readable name, and the ID. See the constants defined at the top of this
+ // file for the keys.
+ scoped_nsobject<NSMutableArray> encodings_;
+
+ IBOutlet NSPopUpButton* encodingsMenu_;
+ NSInteger defaultEncodingIndex_;
+ StringPrefMember defaultEncoding_;
+ BOOL changedEncoding_;
+
+ Profile* profile_; // weak
+}
+
+// Profile cannot be NULL. Caller is responsible for showing the window as a
+// modal sheet.
+- (id)initWithProfile:(Profile*)profile;
+
+// Action for all the font changing buttons. This starts the font picker.
+- (IBAction)selectFont:(id)sender;
+
+// Sent by the FontManager after the user has selected a font.
+- (void)changeFont:(id)fontManager;
+
+// Performs the closing of the window. This is used by both the cancel button
+// and |-save:| after it persists the settings.
+- (IBAction)closeSheet:(id)sender;
+
+// Persists the new values into the preferences and closes the sheet.
+- (IBAction)save:(id)sender;
+
+// Returns the |encodings_| array. This is used by bindings for KVO/KVC.
+- (NSArray*)encodings;
+
+@end
diff --git a/chrome/browser/ui/cocoa/font_language_settings_controller.mm b/chrome/browser/ui/cocoa/font_language_settings_controller.mm
new file mode 100644
index 0000000..b3bdb980
--- /dev/null
+++ b/chrome/browser/ui/cocoa/font_language_settings_controller.mm
@@ -0,0 +1,280 @@
+// 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/ui/cocoa/font_language_settings_controller.h"
+
+#import <Cocoa/Cocoa.h>
+#import "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/character_encoding.h"
+#include "chrome/browser/fonts_languages_window.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/common/pref_names.h"
+
+NSString* const kCharacterInfoEncoding = @"encoding";
+NSString* const kCharacterInfoName = @"name";
+NSString* const kCharacterInfoID = @"id";
+
+void ShowFontsLanguagesWindow(gfx::NativeWindow window,
+ FontsLanguagesPage page,
+ Profile* profile) {
+ NOTIMPLEMENTED();
+}
+
+@interface FontLanguageSettingsController (Private)
+- (void)updateDisplayField:(NSTextField*)field
+ withFont:(NSFont*)font
+ withLabel:(NSTextField*)label;
+@end
+
+@implementation FontLanguageSettingsController
+
+- (id)initWithProfile:(Profile*)profile {
+ DCHECK(profile);
+ NSString* nibpath = [mac_util::MainAppBundle()
+ pathForResource:@"FontLanguageSettings"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ profile_ = profile;
+
+ // Convert the name/size preference values to NSFont objects.
+ serifName_.Init(prefs::kWebKitSerifFontFamily, profile->GetPrefs(), NULL);
+ serifSize_.Init(prefs::kWebKitDefaultFontSize, profile->GetPrefs(), NULL);
+ NSString* serif = base::SysUTF8ToNSString(serifName_.GetValue());
+ serifFont_.reset(
+ [[NSFont fontWithName:serif size:serifSize_.GetValue()] retain]);
+
+ sansSerifName_.Init(prefs::kWebKitSansSerifFontFamily, profile->GetPrefs(),
+ NULL);
+ sansSerifSize_.Init(prefs::kWebKitDefaultFontSize, profile->GetPrefs(),
+ NULL);
+ NSString* sansSerif = base::SysUTF8ToNSString(sansSerifName_.GetValue());
+ sansSerifFont_.reset(
+ [[NSFont fontWithName:sansSerif
+ size:sansSerifSize_.GetValue()] retain]);
+
+ fixedWidthName_.Init(prefs::kWebKitFixedFontFamily, profile->GetPrefs(),
+ NULL);
+ fixedWidthSize_.Init(prefs::kWebKitDefaultFixedFontSize,
+ profile->GetPrefs(), NULL);
+ NSString* fixedWidth = base::SysUTF8ToNSString(fixedWidthName_.GetValue());
+ fixedWidthFont_.reset(
+ [[NSFont fontWithName:fixedWidth
+ size:fixedWidthSize_.GetValue()] retain]);
+
+ // Generate a list of encodings.
+ NSInteger count = CharacterEncoding::GetSupportCanonicalEncodingCount();
+ NSMutableArray* encodings = [NSMutableArray arrayWithCapacity:count];
+ for (NSInteger i = 0; i < count; ++i) {
+ int commandId = CharacterEncoding::GetEncodingCommandIdByIndex(i);
+ string16 name = CharacterEncoding::\
+ GetCanonicalEncodingDisplayNameByCommandId(commandId);
+ std::string encoding =
+ CharacterEncoding::GetCanonicalEncodingNameByCommandId(commandId);
+ NSDictionary* strings = [NSDictionary dictionaryWithObjectsAndKeys:
+ base::SysUTF16ToNSString(name), kCharacterInfoName,
+ base::SysUTF8ToNSString(encoding), kCharacterInfoEncoding,
+ [NSNumber numberWithInt:commandId], kCharacterInfoID,
+ nil
+ ];
+ [encodings addObject:strings];
+ }
+
+ // Sort the encodings.
+ scoped_nsobject<NSSortDescriptor> sorter(
+ [[NSSortDescriptor alloc] initWithKey:kCharacterInfoName
+ ascending:YES]);
+ NSArray* sorterArray = [NSArray arrayWithObject:sorter.get()];
+ encodings_.reset(
+ [[encodings sortedArrayUsingDescriptors:sorterArray] retain]);
+
+ // Find and set the default encoding.
+ defaultEncoding_.Init(prefs::kDefaultCharset, profile->GetPrefs(), NULL);
+ NSString* defaultEncoding =
+ base::SysUTF8ToNSString(defaultEncoding_.GetValue());
+ NSUInteger index = 0;
+ for (NSDictionary* entry in encodings_.get()) {
+ NSString* encoding = [entry objectForKey:kCharacterInfoEncoding];
+ if ([encoding isEqualToString:defaultEncoding]) {
+ defaultEncodingIndex_ = index;
+ break;
+ }
+ ++index;
+ }
+
+ // Register as a KVO observer so we can receive updates when the encoding
+ // changes.
+ [self addObserver:self
+ forKeyPath:@"defaultEncodingIndex_"
+ options:NSKeyValueObservingOptionNew
+ context:NULL];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [self removeObserver:self forKeyPath:@"defaultEncodingIndex_"];
+ [super dealloc];
+}
+
+- (void)awakeFromNib {
+ DCHECK([self window]);
+ [[self window] setDelegate:self];
+
+ // Set up the font display.
+ [self updateDisplayField:serifField_
+ withFont:serifFont_.get()
+ withLabel:serifLabel_];
+ [self updateDisplayField:sansSerifField_
+ withFont:sansSerifFont_.get()
+ withLabel:sansSerifLabel_];
+ [self updateDisplayField:fixedWidthField_
+ withFont:fixedWidthFont_.get()
+ withLabel:fixedWidthLabel_];
+}
+
+- (void)windowWillClose:(NSNotification*)notif {
+ [self autorelease];
+}
+
+- (IBAction)selectFont:(id)sender {
+ if (sender == serifButton_) {
+ currentFont_ = serifFont_.get();
+ currentType_ = FontSettingSerif;
+ } else if (sender == sansSerifButton_) {
+ currentFont_ = sansSerifFont_.get();
+ currentType_ = FontSettingSansSerif;
+ } else if (sender == fixedWidthButton_) {
+ currentFont_ = fixedWidthFont_.get();
+ currentType_ = FontSettingFixed;
+ } else {
+ NOTREACHED();
+ }
+
+ // Validate whatever editing is currently happening.
+ if ([[self window] makeFirstResponder:nil]) {
+ NSFontManager* manager = [NSFontManager sharedFontManager];
+ [manager setTarget:self];
+ [manager setSelectedFont:currentFont_ isMultiple:NO];
+ [manager orderFrontFontPanel:self];
+ }
+}
+
+// Called by the font manager when the user has selected a new font. We should
+// then persist those changes into the preference system.
+- (void)changeFont:(id)fontManager {
+ switch (currentType_) {
+ case FontSettingSerif:
+ serifFont_.reset([[fontManager convertFont:serifFont_] retain]);
+ [self updateDisplayField:serifField_
+ withFont:serifFont_.get()
+ withLabel:serifLabel_];
+ changedSerif_ = YES;
+ break;
+ case FontSettingSansSerif:
+ sansSerifFont_.reset([[fontManager convertFont:sansSerifFont_] retain]);
+ [self updateDisplayField:sansSerifField_
+ withFont:sansSerifFont_.get()
+ withLabel:sansSerifLabel_];
+ changedSansSerif_ = YES;
+ break;
+ case FontSettingFixed:
+ fixedWidthFont_.reset(
+ [[fontManager convertFont:fixedWidthFont_] retain]);
+ [self updateDisplayField:fixedWidthField_
+ withFont:fixedWidthFont_.get()
+ withLabel:fixedWidthLabel_];
+ changedFixedWidth_ = YES;
+ break;
+ default:
+ NOTREACHED();
+ }
+}
+
+- (IBAction)closeSheet:(id)sender {
+ NSFontPanel* panel = [[NSFontManager sharedFontManager] fontPanel:NO];
+ [panel close];
+ [NSApp endSheet:[self window]];
+}
+
+- (IBAction)save:(id)sender {
+ if (changedSerif_) {
+ serifName_.SetValue(base::SysNSStringToUTF8([serifFont_ fontName]));
+ serifSize_.SetValue([serifFont_ pointSize]);
+ }
+ if (changedSansSerif_) {
+ sansSerifName_.SetValue(
+ base::SysNSStringToUTF8([sansSerifFont_ fontName]));
+ sansSerifSize_.SetValue([sansSerifFont_ pointSize]);
+ }
+ if (changedFixedWidth_) {
+ fixedWidthName_.SetValue(
+ base::SysNSStringToUTF8([fixedWidthFont_ fontName]));
+ fixedWidthSize_.SetValue([fixedWidthFont_ pointSize]);
+ }
+ if (changedEncoding_) {
+ NSDictionary* object = [encodings_ objectAtIndex:defaultEncodingIndex_];
+ NSString* newEncoding = [object objectForKey:kCharacterInfoEncoding];
+ std::string encoding = base::SysNSStringToUTF8(newEncoding);
+ defaultEncoding_.SetValue(encoding);
+ }
+ [self closeSheet:sender];
+}
+
+- (NSArray*)encodings {
+ return encodings_.get();
+}
+
+// KVO notification.
+- (void)observeValueForKeyPath:(NSString*)keyPath
+ ofObject:(id)object
+ change:(NSDictionary*)change
+ context:(void*)context {
+ // If this is the default encoding, then set the flag to persist the value.
+ if ([keyPath isEqual:@"defaultEncodingIndex_"]) {
+ changedEncoding_ = YES;
+ return;
+ }
+
+ [super observeValueForKeyPath:keyPath
+ ofObject:object
+ change:change
+ context:context];
+}
+
+#pragma mark Private
+
+// Set the baseline for the font field to be aligned with the baseline
+// of its corresponding label.
+- (NSPoint)getFontFieldOrigin:(NSTextField*)field
+ forLabel:(NSTextField*)label {
+ [field sizeToFit];
+ NSRect labelFrame = [label frame];
+ NSPoint newOrigin =
+ [[label superview] convertPoint:labelFrame.origin
+ toView:[field superview]];
+ newOrigin.x = 0; // Left-align font field.
+ newOrigin.y += [[field font] descender] - [[label font] descender];
+ return newOrigin;
+}
+
+// This will set the font on |field| to be |font|, and will set the string
+// value to something human-readable.
+- (void)updateDisplayField:(NSTextField*)field
+ withFont:(NSFont*)font
+ withLabel:(NSTextField*)label {
+ if (!font) {
+ // Something has gone really wrong. Don't make things worse by showing the
+ // user "(null)".
+ return;
+ }
+ [field setFont:font];
+ NSString* value =
+ [NSString stringWithFormat:@"%@, %g", [font fontName], [font pointSize]];
+ [field setStringValue:value];
+ [field setFrameOrigin:[self getFontFieldOrigin:field forLabel:label]];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/font_language_settings_controller_unittest.mm b/chrome/browser/ui/cocoa/font_language_settings_controller_unittest.mm
new file mode 100644
index 0000000..9b0b259
--- /dev/null
+++ b/chrome/browser/ui/cocoa/font_language_settings_controller_unittest.mm
@@ -0,0 +1,91 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/character_encoding.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/font_language_settings_controller.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+// The FontLanguageSettingsControllerForTest overrides the getFontFieldOrigin
+// method to provide a dummy point, so we don't have to actually display the
+// window to test the controller.
+@interface FontLanguageSettingsControllerForTest :
+ FontLanguageSettingsController {
+}
+
+- (NSPoint)getFontFieldOrigin:(NSTextField*)field
+ forLabel:(NSTextField*)label;
+
+@end
+
+@implementation FontLanguageSettingsControllerForTest
+
+- (NSPoint)getFontFieldOrigin:(NSTextField*)field
+ forLabel:(NSTextField*)label {
+ return NSMakePoint(10, 10);
+}
+
+@end
+
+@interface FontLanguageSettingsController (Testing)
+- (void)updateDisplayField:(NSTextField*)field
+ withFont:(NSFont*)font
+ withLabel:(NSTextField*)label;
+@end
+
+class FontLanguageSettingsControllerTest : public CocoaTest {
+ public:
+ FontLanguageSettingsControllerTest() {
+ Profile* profile = helper_.profile();
+ font_controller_.reset(
+ [[FontLanguageSettingsControllerForTest alloc] initWithProfile:profile]);
+ }
+ ~FontLanguageSettingsControllerTest() {}
+
+ BrowserTestHelper helper_;
+ scoped_nsobject<FontLanguageSettingsController> font_controller_;
+};
+
+TEST_F(FontLanguageSettingsControllerTest, Init) {
+ ASSERT_EQ(CharacterEncoding::GetSupportCanonicalEncodingCount(),
+ static_cast<int>([[font_controller_ encodings] count]));
+}
+
+TEST_F(FontLanguageSettingsControllerTest, UpdateDisplayField) {
+ NSFont* font = [NSFont fontWithName:@"Times-Roman" size:12.0];
+ scoped_nsobject<NSTextField> field(
+ [[NSTextField alloc] initWithFrame:NSMakeRect(100, 100, 100, 100)]);
+ scoped_nsobject<NSTextField> label(
+ [[NSTextField alloc] initWithFrame:NSMakeRect(100, 100, 100, 100)]);
+ [font_controller_ updateDisplayField:field.get()
+ withFont:font
+ withLabel:label];
+
+ ASSERT_NSEQ([font fontName], [[field font] fontName]);
+ ASSERT_NSEQ(@"Times-Roman, 12", [field stringValue]);
+}
+
+TEST_F(FontLanguageSettingsControllerTest, UpdateDisplayFieldNilFont) {
+ scoped_nsobject<NSTextField> field(
+ [[NSTextField alloc] initWithFrame:NSMakeRect(100, 100, 100, 100)]);
+ scoped_nsobject<NSTextField> label(
+ [[NSTextField alloc] initWithFrame:NSMakeRect(100, 100, 100, 100)]);
+ [field setStringValue:@"foo"];
+ [font_controller_ updateDisplayField:field.get()
+ withFont:nil
+ withLabel:label];
+
+ ASSERT_NSEQ(@"foo", [field stringValue]);
+}
+
+TEST_F(FontLanguageSettingsControllerTest, UpdateDisplayFieldNilField) {
+ // Don't crash.
+ NSFont* font = [NSFont fontWithName:@"Times-Roman" size:12.0];
+ [font_controller_ updateDisplayField:nil withFont:font withLabel:nil];
+}
diff --git a/chrome/browser/ui/cocoa/framed_browser_window.h b/chrome/browser/ui/cocoa/framed_browser_window.h
new file mode 100644
index 0000000..40f330c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/framed_browser_window.h
@@ -0,0 +1,65 @@
+// 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_UI_COCOA_FRAMED_BROWSER_WINDOW_H_
+#define CHROME_BROWSER_UI_COCOA_FRAMED_BROWSER_WINDOW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/ui/cocoa/chrome_browser_window.h"
+
+// Offset from the top of the window frame to the top of the window controls
+// (zoom, close, miniaturize) for a window with a tabstrip.
+const NSInteger kFramedWindowButtonsWithTabStripOffsetFromTop = 6;
+
+// Offset from the top of the window frame to the top of the window controls
+// (zoom, close, miniaturize) for a window without a tabstrip.
+const NSInteger kFramedWindowButtonsWithoutTabStripOffsetFromTop = 4;
+
+// Offset from the left of the window frame to the top of the window controls
+// (zoom, close, miniaturize).
+const NSInteger kFramedWindowButtonsOffsetFromLeft = 8;
+
+// Offset between the window controls (zoom, close, miniaturize).
+const NSInteger kFramedWindowButtonsInterButtonSpacing = 7;
+
+// Cocoa class representing a framed browser window.
+// We need to override NSWindow with our own class since we need access to all
+// unhandled keyboard events and subclassing NSWindow is the only method to do
+// this. We also handle our own window controls and custom window frame drawing.
+@interface FramedBrowserWindow : ChromeBrowserWindow {
+ @private
+ BOOL shouldHideTitle_;
+ NSButton* closeButton_;
+ NSButton* miniaturizeButton_;
+ NSButton* zoomButton_;
+ BOOL entered_;
+ scoped_nsobject<NSTrackingArea> widgetTrackingArea_;
+}
+
+// Tells the window to suppress title drawing.
+- (void)setShouldHideTitle:(BOOL)flag;
+
+// Return true if the mouse is currently in our tracking area for our window
+// widgets.
+- (BOOL)mouseInGroup:(NSButton*)widget;
+
+// Update the tracking areas for our window widgets as appropriate.
+- (void)updateTrackingAreas;
+
+@end
+
+@interface NSWindow (UndocumentedAPI)
+
+// Undocumented Cocoa API to suppress drawing of the window's title.
+// -setTitle: still works, but the title set only applies to the
+// miniwindow and menus (and, importantly, Expose). Overridden to
+// return |shouldHideTitle_|.
+-(BOOL)_isTitleHidden;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_FRAMED_BROWSER_WINDOW_H_
diff --git a/chrome/browser/ui/cocoa/framed_browser_window.mm b/chrome/browser/ui/cocoa/framed_browser_window.mm
new file mode 100644
index 0000000..9d63cb7
--- /dev/null
+++ b/chrome/browser/ui/cocoa/framed_browser_window.mm
@@ -0,0 +1,350 @@
+// 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/ui/cocoa/framed_browser_window.h"
+
+#include "base/logging.h"
+#include "chrome/browser/global_keyboard_shortcuts_mac.h"
+#import "chrome/browser/ui/cocoa/browser_frame_view.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/tab_strip_controller.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#import "chrome/browser/renderer_host/render_widget_host_view_mac.h"
+#include "chrome/browser/themes/browser_theme_provider.h"
+
+namespace {
+ // Size of the gradient. Empirically determined so that the gradient looks
+ // like what the heuristic does when there are just a few tabs.
+ const CGFloat kWindowGradientHeight = 24.0;
+}
+
+// Our browser window does some interesting things to get the behaviors that
+// we want. We replace the standard window controls (zoom, close, miniaturize)
+// with our own versions, so that we can position them slightly differently than
+// the default window has them. To do this, we hide the ones that Apple provides
+// us with, and create our own. This requires us to handle tracking for the
+// buttons (so that they highlight and activate correctly) as well as implement
+// the private method _mouseInGroup in our frame view class which is required
+// to get the rollover highlight drawing to draw correctly.
+@interface FramedBrowserWindow(PrivateMethods)
+// Return the view that does the "frame" drawing.
+- (NSView*)frameView;
+@end
+
+@implementation FramedBrowserWindow
+
+- (id)initWithContentRect:(NSRect)contentRect
+ styleMask:(NSUInteger)aStyle
+ backing:(NSBackingStoreType)bufferingType
+ defer:(BOOL)flag {
+ if ((self = [super initWithContentRect:contentRect
+ styleMask:aStyle
+ backing:bufferingType
+ defer:flag])) {
+ if (aStyle & NSTexturedBackgroundWindowMask) {
+ // The following two calls fix http://www.crbug.com/25684 by preventing
+ // the window from recalculating the border thickness as the window is
+ // resized.
+ // This was causing the window tint to change for the default system theme
+ // when the window was being resized.
+ [self setAutorecalculatesContentBorderThickness:NO forEdge:NSMaxYEdge];
+ [self setContentBorderThickness:kWindowGradientHeight forEdge:NSMaxYEdge];
+ }
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
+ if (widgetTrackingArea_) {
+ [[self frameView] removeTrackingArea:widgetTrackingArea_];
+ widgetTrackingArea_.reset();
+ }
+ [super dealloc];
+}
+
+- (void)setWindowController:(NSWindowController*)controller {
+ if (controller == [self windowController]) {
+ return;
+ }
+ // Clean up our old stuff.
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
+ [closeButton_ removeFromSuperview];
+ closeButton_ = nil;
+ [miniaturizeButton_ removeFromSuperview];
+ miniaturizeButton_ = nil;
+ [zoomButton_ removeFromSuperview];
+ zoomButton_ = nil;
+
+ [super setWindowController:controller];
+
+ BrowserWindowController* browserController
+ = static_cast<BrowserWindowController*>(controller);
+ if ([browserController isKindOfClass:[BrowserWindowController class]]) {
+ NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
+ [defaultCenter addObserver:self
+ selector:@selector(themeDidChangeNotification:)
+ name:kBrowserThemeDidChangeNotification
+ object:nil];
+
+ // Hook ourselves up to get notified if the user changes the system
+ // theme on us.
+ NSDistributedNotificationCenter* distCenter =
+ [NSDistributedNotificationCenter defaultCenter];
+ [distCenter addObserver:self
+ selector:@selector(systemThemeDidChangeNotification:)
+ name:@"AppleAquaColorVariantChanged"
+ object:nil];
+ // Set up our buttons how we like them.
+ NSView* frameView = [self frameView];
+ NSRect frameViewBounds = [frameView bounds];
+
+ // Find all the "original" buttons, and hide them. We can't use the original
+ // buttons because the OS likes to move them around when we resize windows
+ // and will put them back in what it considers to be their "preferred"
+ // locations.
+ NSButton* oldButton = [self standardWindowButton:NSWindowCloseButton];
+ [oldButton setHidden:YES];
+ oldButton = [self standardWindowButton:NSWindowMiniaturizeButton];
+ [oldButton setHidden:YES];
+ oldButton = [self standardWindowButton:NSWindowZoomButton];
+ [oldButton setHidden:YES];
+
+ // Create and position our new buttons.
+ NSUInteger aStyle = [self styleMask];
+ closeButton_ = [NSWindow standardWindowButton:NSWindowCloseButton
+ forStyleMask:aStyle];
+ NSRect closeButtonFrame = [closeButton_ frame];
+ CGFloat yOffset = [browserController hasTabStrip] ?
+ kFramedWindowButtonsWithTabStripOffsetFromTop :
+ kFramedWindowButtonsWithoutTabStripOffsetFromTop;
+ closeButtonFrame.origin =
+ NSMakePoint(kFramedWindowButtonsOffsetFromLeft,
+ (NSHeight(frameViewBounds) -
+ NSHeight(closeButtonFrame) - yOffset));
+
+ [closeButton_ setFrame:closeButtonFrame];
+ [closeButton_ setTarget:self];
+ [closeButton_ setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
+ [frameView addSubview:closeButton_];
+
+ miniaturizeButton_ =
+ [NSWindow standardWindowButton:NSWindowMiniaturizeButton
+ forStyleMask:aStyle];
+ NSRect miniaturizeButtonFrame = [miniaturizeButton_ frame];
+ miniaturizeButtonFrame.origin =
+ NSMakePoint((NSMaxX(closeButtonFrame) +
+ kFramedWindowButtonsInterButtonSpacing),
+ NSMinY(closeButtonFrame));
+ [miniaturizeButton_ setFrame:miniaturizeButtonFrame];
+ [miniaturizeButton_ setTarget:self];
+ [miniaturizeButton_ setAutoresizingMask:(NSViewMaxXMargin |
+ NSViewMinYMargin)];
+ [frameView addSubview:miniaturizeButton_];
+
+ zoomButton_ = [NSWindow standardWindowButton:NSWindowZoomButton
+ forStyleMask:aStyle];
+ NSRect zoomButtonFrame = [zoomButton_ frame];
+ zoomButtonFrame.origin =
+ NSMakePoint((NSMaxX(miniaturizeButtonFrame) +
+ kFramedWindowButtonsInterButtonSpacing),
+ NSMinY(miniaturizeButtonFrame));
+ [zoomButton_ setFrame:zoomButtonFrame];
+ [zoomButton_ setTarget:self];
+ [zoomButton_ setAutoresizingMask:(NSViewMaxXMargin |
+ NSViewMinYMargin)];
+
+ [frameView addSubview:zoomButton_];
+ }
+
+ // Update our tracking areas. We want to update them even if we haven't
+ // added buttons above as we need to remove the old tracking area. If the
+ // buttons aren't to be shown, updateTrackingAreas won't add new ones.
+ [self updateTrackingAreas];
+}
+
+- (NSView*)frameView {
+ return [[self contentView] superview];
+}
+
+// The tab strip view covers our window buttons. So we add hit testing here
+// to find them properly and return them to the accessibility system.
+- (id)accessibilityHitTest:(NSPoint)point {
+ NSPoint windowPoint = [self convertScreenToBase:point];
+ NSControl* controls[] = { closeButton_, zoomButton_, miniaturizeButton_ };
+ id value = nil;
+ for (size_t i = 0; i < sizeof(controls) / sizeof(controls[0]); ++i) {
+ if (NSPointInRect(windowPoint, [controls[i] frame])) {
+ value = [controls[i] accessibilityHitTest:point];
+ break;
+ }
+ }
+ if (!value) {
+ value = [super accessibilityHitTest:point];
+ }
+ return value;
+}
+
+// Map our custom buttons into the accessibility hierarchy correctly.
+- (id)accessibilityAttributeValue:(NSString*)attribute {
+ id value = nil;
+ struct {
+ NSString* attribute_;
+ id value_;
+ } attributeMap[] = {
+ { NSAccessibilityCloseButtonAttribute, [closeButton_ cell]},
+ { NSAccessibilityZoomButtonAttribute, [zoomButton_ cell]},
+ { NSAccessibilityMinimizeButtonAttribute, [miniaturizeButton_ cell]},
+ };
+
+ for (size_t i = 0; i < sizeof(attributeMap) / sizeof(attributeMap[0]); ++i) {
+ if ([attributeMap[i].attribute_ isEqualToString:attribute]) {
+ value = attributeMap[i].value_;
+ break;
+ }
+ }
+ if (!value) {
+ value = [super accessibilityAttributeValue:attribute];
+ }
+ return value;
+}
+
+- (void)updateTrackingAreas {
+ NSView* frameView = [self frameView];
+ if (widgetTrackingArea_) {
+ [frameView removeTrackingArea:widgetTrackingArea_];
+ }
+ if (closeButton_) {
+ NSRect trackingRect = [closeButton_ frame];
+ trackingRect.size.width = NSMaxX([zoomButton_ frame]) -
+ NSMinX(trackingRect);
+ widgetTrackingArea_.reset(
+ [[NSTrackingArea alloc] initWithRect:trackingRect
+ options:(NSTrackingMouseEnteredAndExited |
+ NSTrackingActiveAlways)
+ owner:self
+ userInfo:nil]);
+ [frameView addTrackingArea:widgetTrackingArea_];
+
+ // Check to see if the cursor is still in trackingRect.
+ NSPoint point = [self mouseLocationOutsideOfEventStream];
+ point = [[self contentView] convertPoint:point fromView:nil];
+ BOOL newEntered = NSPointInRect (point, trackingRect);
+ if (newEntered != entered_) {
+ // Buttons have moved, so update button state.
+ entered_ = newEntered;
+ [closeButton_ setNeedsDisplay];
+ [zoomButton_ setNeedsDisplay];
+ [miniaturizeButton_ setNeedsDisplay];
+ }
+ }
+}
+
+- (void)windowMainStatusChanged {
+ [closeButton_ setNeedsDisplay];
+ [zoomButton_ setNeedsDisplay];
+ [miniaturizeButton_ setNeedsDisplay];
+ NSView* frameView = [self frameView];
+ NSView* contentView = [self contentView];
+ NSRect updateRect = [frameView frame];
+ NSRect contentRect = [contentView frame];
+ CGFloat tabStripHeight = [TabStripController defaultTabHeight];
+ updateRect.size.height -= NSHeight(contentRect) - tabStripHeight;
+ updateRect.origin.y = NSMaxY(contentRect) - tabStripHeight;
+ [[self frameView] setNeedsDisplayInRect:updateRect];
+}
+
+- (void)becomeMainWindow {
+ [self windowMainStatusChanged];
+ [super becomeMainWindow];
+}
+
+- (void)resignMainWindow {
+ [self windowMainStatusChanged];
+ [super resignMainWindow];
+}
+
+// Called after the current theme has changed.
+- (void)themeDidChangeNotification:(NSNotification*)aNotification {
+ [[self frameView] setNeedsDisplay:YES];
+}
+
+- (void)systemThemeDidChangeNotification:(NSNotification*)aNotification {
+ [closeButton_ setNeedsDisplay];
+ [zoomButton_ setNeedsDisplay];
+ [miniaturizeButton_ setNeedsDisplay];
+}
+
+- (void)sendEvent:(NSEvent*)event {
+ // For cocoa windows, clicking on the close and the miniaturize (but not the
+ // zoom buttons) while a window is in the background does NOT bring that
+ // window to the front. We don't get that behavior for free, so we handle
+ // it here. Zoom buttons do bring the window to the front. Note that
+ // Finder windows (in Leopard) behave differently in this regard in that
+ // zoom buttons don't bring the window to the foreground.
+ BOOL eventHandled = NO;
+ if (![self isMainWindow]) {
+ if ([event type] == NSLeftMouseDown) {
+ NSView* frameView = [self frameView];
+ NSPoint mouse = [frameView convertPoint:[event locationInWindow]
+ fromView:nil];
+ if (NSPointInRect(mouse, [closeButton_ frame])) {
+ [closeButton_ mouseDown:event];
+ eventHandled = YES;
+ } else if (NSPointInRect(mouse, [miniaturizeButton_ frame])) {
+ [miniaturizeButton_ mouseDown:event];
+ eventHandled = YES;
+ }
+ }
+ }
+ if (!eventHandled) {
+ [super sendEvent:event];
+ }
+}
+
+// Update our buttons so that they highlight correctly.
+- (void)mouseEntered:(NSEvent*)event {
+ entered_ = YES;
+ [closeButton_ setNeedsDisplay];
+ [zoomButton_ setNeedsDisplay];
+ [miniaturizeButton_ setNeedsDisplay];
+}
+
+// Update our buttons so that they highlight correctly.
+- (void)mouseExited:(NSEvent*)event {
+ entered_ = NO;
+ [closeButton_ setNeedsDisplay];
+ [zoomButton_ setNeedsDisplay];
+ [miniaturizeButton_ setNeedsDisplay];
+}
+
+- (BOOL)mouseInGroup:(NSButton*)widget {
+ return entered_;
+}
+
+- (void)setShouldHideTitle:(BOOL)flag {
+ shouldHideTitle_ = flag;
+}
+
+-(BOOL)_isTitleHidden {
+ return shouldHideTitle_;
+}
+
+// This method is called whenever a window is moved in order to ensure it fits
+// on the screen. We cannot always handle resizes without breaking, so we
+// prevent frame constraining in those cases.
+- (NSRect)constrainFrameRect:(NSRect)frame toScreen:(NSScreen*)screen {
+ // Do not constrain the frame rect if our delegate says no. In this case,
+ // return the original (unconstrained) frame.
+ id delegate = [self delegate];
+ if ([delegate respondsToSelector:@selector(shouldConstrainFrameRect)] &&
+ ![delegate shouldConstrainFrameRect])
+ return frame;
+
+ return [super constrainFrameRect:frame toScreen:screen];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/framed_browser_window_unittest.mm b/chrome/browser/ui/cocoa/framed_browser_window_unittest.mm
new file mode 100644
index 0000000..ad05334
--- /dev/null
+++ b/chrome/browser/ui/cocoa/framed_browser_window_unittest.mm
@@ -0,0 +1,184 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/debug/debugger.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/app/chrome_command_ids.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/browser_frame_view.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/framed_browser_window.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+
+class FramedBrowserWindowTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ // Create a window.
+ const NSUInteger mask = NSTitledWindowMask | NSClosableWindowMask |
+ NSMiniaturizableWindowMask | NSResizableWindowMask;
+ window_ = [[FramedBrowserWindow alloc]
+ initWithContentRect:NSMakeRect(0, 0, 800, 600)
+ styleMask:mask
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ if (base::debug::BeingDebugged()) {
+ [window_ orderFront:nil];
+ } else {
+ [window_ orderBack:nil];
+ }
+ }
+
+ virtual void TearDown() {
+ [window_ close];
+ CocoaTest::TearDown();
+ }
+
+ // Returns a canonical snapshot of the window.
+ NSData* WindowContentsAsTIFF() {
+ [window_ display];
+
+ NSView* frameView = [window_ contentView];
+ while ([frameView superview]) {
+ frameView = [frameView superview];
+ }
+ const NSRect bounds = [frameView bounds];
+
+ [frameView lockFocus];
+ scoped_nsobject<NSBitmapImageRep> bitmap(
+ [[NSBitmapImageRep alloc] initWithFocusedViewRect:bounds]);
+ [frameView unlockFocus];
+
+ return [bitmap TIFFRepresentation];
+ }
+
+ FramedBrowserWindow* window_;
+};
+
+// Baseline test that the window creates, displays, closes, and
+// releases.
+TEST_F(FramedBrowserWindowTest, ShowAndClose) {
+ [window_ display];
+}
+
+// Test that undocumented title-hiding API we're using does the job.
+TEST_F(FramedBrowserWindowTest, DoesHideTitle) {
+ // The -display calls are not strictly necessary, but they do
+ // make it easier to see what's happening when debugging (without
+ // them the changes are never flushed to the screen).
+
+ [window_ setTitle:@""];
+ [window_ display];
+ NSData* emptyTitleData = WindowContentsAsTIFF();
+
+ [window_ setTitle:@"This is a title"];
+ [window_ display];
+ NSData* thisTitleData = WindowContentsAsTIFF();
+
+ // The default window with a title should look different from the
+ // window with an empty title.
+ EXPECT_FALSE([emptyTitleData isEqualToData:thisTitleData]);
+
+ [window_ setShouldHideTitle:YES];
+ [window_ setTitle:@""];
+ [window_ display];
+ [window_ setTitle:@"This is a title"];
+ [window_ display];
+ NSData* hiddenTitleData = WindowContentsAsTIFF();
+
+ // With our magic setting, the window with a title should look the
+ // same as the window with an empty title.
+ EXPECT_TRUE([window_ _isTitleHidden]);
+ EXPECT_TRUE([emptyTitleData isEqualToData:hiddenTitleData]);
+}
+
+// Test to make sure that our window widgets are in the right place.
+TEST_F(FramedBrowserWindowTest, WindowWidgetLocation) {
+ // First without tabstrip.
+ NSCell* closeBoxCell = [window_ accessibilityAttributeValue:
+ NSAccessibilityCloseButtonAttribute];
+ NSView* closeBoxControl = [closeBoxCell controlView];
+ EXPECT_TRUE(closeBoxControl);
+ NSRect closeBoxFrame = [closeBoxControl frame];
+ NSRect windowBounds = [window_ frame];
+ windowBounds.origin = NSZeroPoint;
+ EXPECT_EQ(NSMaxY(closeBoxFrame),
+ NSMaxY(windowBounds) -
+ kFramedWindowButtonsWithoutTabStripOffsetFromTop);
+ EXPECT_EQ(NSMinX(closeBoxFrame), kFramedWindowButtonsOffsetFromLeft);
+
+ NSCell* miniaturizeCell = [window_ accessibilityAttributeValue:
+ NSAccessibilityMinimizeButtonAttribute];
+ NSView* miniaturizeControl = [miniaturizeCell controlView];
+ EXPECT_TRUE(miniaturizeControl);
+ NSRect miniaturizeFrame = [miniaturizeControl frame];
+ EXPECT_EQ(NSMaxY(miniaturizeFrame),
+ NSMaxY(windowBounds) -
+ kFramedWindowButtonsWithoutTabStripOffsetFromTop);
+ EXPECT_EQ(NSMinX(miniaturizeFrame),
+ NSMaxX(closeBoxFrame) + kFramedWindowButtonsInterButtonSpacing);
+
+ // Then with a tabstrip.
+ id controller = [OCMockObject mockForClass:[BrowserWindowController class]];
+ BOOL yes = YES;
+ BOOL no = NO;
+ [[[controller stub] andReturnValue:OCMOCK_VALUE(yes)]
+ isKindOfClass:[BrowserWindowController class]];
+ [[[controller expect] andReturnValue:OCMOCK_VALUE(yes)] hasTabStrip];
+ [[[controller expect] andReturnValue:OCMOCK_VALUE(no)] hasTitleBar];
+ [[[controller expect] andReturnValue:OCMOCK_VALUE(yes)] isNormalWindow];
+ [window_ setWindowController:controller];
+
+ closeBoxCell = [window_ accessibilityAttributeValue:
+ NSAccessibilityCloseButtonAttribute];
+ closeBoxControl = [closeBoxCell controlView];
+ EXPECT_TRUE(closeBoxControl);
+ closeBoxFrame = [closeBoxControl frame];
+ windowBounds = [window_ frame];
+ windowBounds.origin = NSZeroPoint;
+ EXPECT_EQ(NSMaxY(closeBoxFrame),
+ NSMaxY(windowBounds) -
+ kFramedWindowButtonsWithTabStripOffsetFromTop);
+ EXPECT_EQ(NSMinX(closeBoxFrame), kFramedWindowButtonsOffsetFromLeft);
+
+ miniaturizeCell = [window_ accessibilityAttributeValue:
+ NSAccessibilityMinimizeButtonAttribute];
+ miniaturizeControl = [miniaturizeCell controlView];
+ EXPECT_TRUE(miniaturizeControl);
+ miniaturizeFrame = [miniaturizeControl frame];
+ EXPECT_EQ(NSMaxY(miniaturizeFrame),
+ NSMaxY(windowBounds) -
+ kFramedWindowButtonsWithTabStripOffsetFromTop);
+ EXPECT_EQ(NSMinX(miniaturizeFrame),
+ NSMaxX(closeBoxFrame) + kFramedWindowButtonsInterButtonSpacing);
+ [window_ setWindowController:nil];
+}
+
+// Test that we actually have a tracking area in place.
+TEST_F(FramedBrowserWindowTest, WindowWidgetTrackingArea) {
+ NSCell* closeBoxCell =
+ [window_ accessibilityAttributeValue:NSAccessibilityCloseButtonAttribute];
+ NSView* closeBoxControl = [closeBoxCell controlView];
+ NSView* frameView = [[window_ contentView] superview];
+ NSArray* trackingAreas = [frameView trackingAreas];
+ NSPoint point = [closeBoxControl frame].origin;
+ point.x += 1;
+ point.y += 1;
+ BOOL foundArea = NO;
+ for (NSTrackingArea* area in trackingAreas) {
+ NSRect rect = [area rect];
+ foundArea = NSPointInRect(point, rect);
+ if (foundArea) {
+ EXPECT_NSEQ(frameView, [area owner]);
+ break;
+ }
+ }
+ EXPECT_TRUE(foundArea);
+}
+
diff --git a/chrome/browser/ui/cocoa/fullscreen_controller.h b/chrome/browser/ui/cocoa/fullscreen_controller.h
new file mode 100644
index 0000000..2e96b61
--- /dev/null
+++ b/chrome/browser/ui/cocoa/fullscreen_controller.h
@@ -0,0 +1,122 @@
+// 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_UI_COCOA_FULLSCREEN_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_FULLSCREEN_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/cocoa_protocols_mac.h"
+#include "base/mac_util.h"
+#include "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
+
+@class BrowserWindowController;
+@class DropdownAnimation;
+
+// Provides a controller to manage fullscreen mode for a single browser window.
+// This class handles running animations, showing and hiding the floating
+// dropdown bar, and managing the tracking area associated with the dropdown.
+// This class does not directly manage any views -- the BrowserWindowController
+// is responsible for positioning and z-ordering views.
+//
+// Tracking areas are disabled while animations are running. If
+// |overlayFrameChanged:| is called while an animation is running, the
+// controller saves the new frame and installs the appropriate tracking area
+// when the animation finishes. This is largely done for ease of
+// implementation; it is easier to check the mouse location at each animation
+// step than it is to manage a constantly-changing tracking area.
+@interface FullscreenController : NSObject<NSAnimationDelegate> {
+ @private
+ // Our parent controller.
+ BrowserWindowController* browserController_; // weak
+
+ // The content view for the fullscreen window. This is nil when not in
+ // fullscreen mode.
+ NSView* contentView_; // weak
+
+ // Whether or not we are in fullscreen mode.
+ BOOL isFullscreen_;
+
+ // The tracking area associated with the floating dropdown bar. This tracking
+ // area is attached to |contentView_|, because when the dropdown is completely
+ // hidden, we still need to keep a 1px tall tracking area visible. Attaching
+ // to the content view allows us to do this. |trackingArea_| can be nil if
+ // not in fullscreen mode or during animations.
+ scoped_nsobject<NSTrackingArea> trackingArea_;
+
+ // Pointer to the currently running animation. Is nil if no animation is
+ // running.
+ scoped_nsobject<DropdownAnimation> currentAnimation_;
+
+ // Timers for scheduled showing/hiding of the bar (which are always done with
+ // animation).
+ scoped_nsobject<NSTimer> showTimer_;
+ scoped_nsobject<NSTimer> hideTimer_;
+
+ // Holds the current bounds of |trackingArea_|, even if |trackingArea_| is
+ // currently nil. Used to restore the tracking area when an animation
+ // completes.
+ NSRect trackingAreaBounds_;
+
+ // Tracks the currently requested fullscreen mode. This should be
+ // |kFullScreenModeNormal| when the window is not main or not fullscreen,
+ // |kFullScreenModeHideAll| while the overlay is hidden, and
+ // |kFullScreenModeHideDock| while the overlay is shown. If the window is not
+ // on the primary screen, this should always be |kFullScreenModeNormal|. This
+ // value can get out of sync with the correct state if we miss a notification
+ // (which can happen when a fullscreen window is closed). Used to track the
+ // current state and make sure we properly restore the menu bar when this
+ // controller is destroyed.
+ mac_util::FullScreenMode currentFullscreenMode_;
+}
+
+@property(readonly, nonatomic) BOOL isFullscreen;
+
+// Designated initializer.
+- (id)initWithBrowserController:(BrowserWindowController*)controller;
+
+// Informs the controller that the browser has entered or exited fullscreen
+// mode. |-enterFullscreenForContentView:showDropdown:| should be called after
+// the fullscreen window is setup, just before it is shown. |-exitFullscreen|
+// should be called before any views are moved back to the non-fullscreen
+// window. If |-enterFullscreenForContentView:showDropdown:| is called, it must
+// be followed with a call to |-exitFullscreen| before the controller is
+// released.
+- (void)enterFullscreenForContentView:(NSView*)contentView
+ showDropdown:(BOOL)showDropdown;
+- (void)exitFullscreen;
+
+// Returns the amount by which the floating bar should be offset downwards (to
+// avoid the menu) and by which the overlay view should be enlarged vertically.
+// Generally, this is > 0 when the fullscreen window is on the primary screen
+// and 0 otherwise.
+- (CGFloat)floatingBarVerticalOffset;
+
+// Informs the controller that the overlay's frame has changed. The controller
+// uses this information to update its tracking areas.
+- (void)overlayFrameChanged:(NSRect)frame;
+
+// Informs the controller that the overlay should be shown/hidden, possibly with
+// animation, possibly after a delay (only applicable for the animated case).
+- (void)ensureOverlayShownWithAnimation:(BOOL)animate delay:(BOOL)delay;
+- (void)ensureOverlayHiddenWithAnimation:(BOOL)animate delay:(BOOL)delay;
+
+// Cancels any running animation and timers.
+- (void)cancelAnimationAndTimers;
+
+// Gets the current floating bar shown fraction.
+- (CGFloat)floatingBarShownFraction;
+
+// Sets a new current floating bar shown fraction. NOTE: This function has side
+// effects, such as modifying the fullscreen mode (menu bar shown state).
+- (void)changeFloatingBarShownFraction:(CGFloat)fraction;
+
+@end
+
+// Notification posted when we're about to enter or leave fullscreen.
+extern NSString* const kWillEnterFullscreenNotification;
+extern NSString* const kWillLeaveFullscreenNotification;
+
+#endif // CHROME_BROWSER_UI_COCOA_FULLSCREEN_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/fullscreen_controller.mm b/chrome/browser/ui/cocoa/fullscreen_controller.mm
new file mode 100644
index 0000000..0f06e22
--- /dev/null
+++ b/chrome/browser/ui/cocoa/fullscreen_controller.mm
@@ -0,0 +1,633 @@
+// 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/ui/cocoa/fullscreen_controller.h"
+
+#include <algorithm>
+
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+
+NSString* const kWillEnterFullscreenNotification =
+ @"WillEnterFullscreenNotification";
+NSString* const kWillLeaveFullscreenNotification =
+ @"WillLeaveFullscreenNotification";
+
+namespace {
+// The activation zone for the main menu is 4 pixels high; if we make it any
+// smaller, then the menu can be made to appear without the bar sliding down.
+const CGFloat kDropdownActivationZoneHeight = 4;
+const NSTimeInterval kDropdownAnimationDuration = 0.12;
+const NSTimeInterval kMouseExitCheckDelay = 0.1;
+// This show delay attempts to match the delay for the main menu.
+const NSTimeInterval kDropdownShowDelay = 0.3;
+const NSTimeInterval kDropdownHideDelay = 0.2;
+
+// The amount by which the floating bar is offset downwards (to avoid the menu)
+// in fullscreen mode. (We can't use |-[NSMenu menuBarHeight]| since it returns
+// 0 when the menu bar is hidden.)
+const CGFloat kFloatingBarVerticalOffset = 22;
+
+} // end namespace
+
+
+// Helper class to manage animations for the fullscreen dropdown bar. Calls
+// [FullscreenController changeFloatingBarShownFraction] once per animation
+// step.
+@interface DropdownAnimation : NSAnimation {
+ @private
+ FullscreenController* controller_;
+ CGFloat startFraction_;
+ CGFloat endFraction_;
+}
+
+@property(readonly, nonatomic) CGFloat startFraction;
+@property(readonly, nonatomic) CGFloat endFraction;
+
+// Designated initializer. Asks |controller| for the current shown fraction, so
+// if the bar is already partially shown or partially hidden, the animation
+// duration may be less than |fullDuration|.
+- (id)initWithFraction:(CGFloat)fromFraction
+ fullDuration:(CGFloat)fullDuration
+ animationCurve:(NSInteger)animationCurve
+ controller:(FullscreenController*)controller;
+
+@end
+
+@implementation DropdownAnimation
+
+@synthesize startFraction = startFraction_;
+@synthesize endFraction = endFraction_;
+
+- (id)initWithFraction:(CGFloat)toFraction
+ fullDuration:(CGFloat)fullDuration
+ animationCurve:(NSInteger)animationCurve
+ controller:(FullscreenController*)controller {
+ // Calculate the effective duration, based on the current shown fraction.
+ DCHECK(controller);
+ CGFloat fromFraction = [controller floatingBarShownFraction];
+ CGFloat effectiveDuration = fabs(fullDuration * (fromFraction - toFraction));
+
+ if ((self = [super gtm_initWithDuration:effectiveDuration
+ eventMask:NSLeftMouseDownMask
+ animationCurve:animationCurve])) {
+ startFraction_ = fromFraction;
+ endFraction_ = toFraction;
+ controller_ = controller;
+ }
+ return self;
+}
+
+// Called once per animation step. Overridden to change the floating bar's
+// position based on the animation's progress.
+- (void)setCurrentProgress:(NSAnimationProgress)progress {
+ CGFloat fraction =
+ startFraction_ + (progress * (endFraction_ - startFraction_));
+ [controller_ changeFloatingBarShownFraction:fraction];
+}
+
+@end
+
+
+@interface FullscreenController (PrivateMethods)
+
+// Returns YES if the fullscreen window is on the primary screen.
+- (BOOL)isWindowOnPrimaryScreen;
+
+// Returns YES if it is ok to show and hide the menu bar in response to the
+// overlay opening and closing. Will return NO if the window is not main or not
+// on the primary monitor.
+- (BOOL)shouldToggleMenuBar;
+
+// Returns |kFullScreenModeHideAll| when the overlay is hidden and
+// |kFullScreenModeHideDock| when the overlay is shown.
+- (mac_util::FullScreenMode)desiredFullscreenMode;
+
+// Change the overlay to the given fraction, with or without animation. Only
+// guaranteed to work properly with |fraction == 0| or |fraction == 1|. This
+// performs the show/hide (animation) immediately. It does not touch the timers.
+- (void)changeOverlayToFraction:(CGFloat)fraction
+ withAnimation:(BOOL)animate;
+
+// Schedule the floating bar to be shown/hidden because of mouse position.
+- (void)scheduleShowForMouse;
+- (void)scheduleHideForMouse;
+
+// Set up the tracking area used to activate the sliding bar or keep it active
+// using with the rectangle in |trackingAreaBounds_|, or remove the tracking
+// area if one was previously set up.
+- (void)setupTrackingArea;
+- (void)removeTrackingAreaIfNecessary;
+
+// Returns YES if the mouse is currently in any current tracking rectangle, NO
+// otherwise.
+- (BOOL)mouseInsideTrackingRect;
+
+// The tracking area can "falsely" report exits when the menu slides down over
+// it. In that case, we have to monitor for a "real" mouse exit on a timer.
+// |-setupMouseExitCheck| schedules a check; |-cancelMouseExitCheck| cancels any
+// scheduled check.
+- (void)setupMouseExitCheck;
+- (void)cancelMouseExitCheck;
+
+// Called (after a delay) by |-setupMouseExitCheck|, to check whether the mouse
+// has exited or not; if it hasn't, it will schedule another check.
+- (void)checkForMouseExit;
+
+// Start timers for showing/hiding the floating bar.
+- (void)startShowTimer;
+- (void)startHideTimer;
+- (void)cancelShowTimer;
+- (void)cancelHideTimer;
+- (void)cancelAllTimers;
+
+// Methods called when the show/hide timers fire. Do not call directly.
+- (void)showTimerFire:(NSTimer*)timer;
+- (void)hideTimerFire:(NSTimer*)timer;
+
+// Stops any running animations, removes tracking areas, etc.
+- (void)cleanup;
+
+// Shows and hides the UI associated with this window being active (having main
+// status). This includes hiding the menu bar and displaying the "Exit
+// Fullscreen" button. These functions are called when the window gains or
+// loses main status as well as in |-cleanup|.
+- (void)showActiveWindowUI;
+- (void)hideActiveWindowUI;
+
+@end
+
+
+@implementation FullscreenController
+
+@synthesize isFullscreen = isFullscreen_;
+
+- (id)initWithBrowserController:(BrowserWindowController*)controller {
+ if ((self == [super init])) {
+ browserController_ = controller;
+ currentFullscreenMode_ = mac_util::kFullScreenModeNormal;
+ }
+
+ // Let the world know what we're up to.
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kWillEnterFullscreenNotification
+ object:nil];
+
+ return self;
+}
+
+- (void)dealloc {
+ DCHECK(!isFullscreen_);
+ DCHECK(!trackingArea_);
+ [super dealloc];
+}
+
+- (void)enterFullscreenForContentView:(NSView*)contentView
+ showDropdown:(BOOL)showDropdown {
+ DCHECK(!isFullscreen_);
+ isFullscreen_ = YES;
+ contentView_ = contentView;
+ [self changeFloatingBarShownFraction:(showDropdown ? 1 : 0)];
+
+ // Register for notifications. Self is removed as an observer in |-cleanup|.
+ NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
+ NSWindow* window = [browserController_ window];
+ [nc addObserver:self
+ selector:@selector(windowDidChangeScreen:)
+ name:NSWindowDidChangeScreenNotification
+ object:window];
+
+ [nc addObserver:self
+ selector:@selector(windowDidMove:)
+ name:NSWindowDidMoveNotification
+ object:window];
+
+ [nc addObserver:self
+ selector:@selector(windowDidBecomeMain:)
+ name:NSWindowDidBecomeMainNotification
+ object:window];
+
+ [nc addObserver:self
+ selector:@selector(windowDidResignMain:)
+ name:NSWindowDidResignMainNotification
+ object:window];
+}
+
+- (void)exitFullscreen {
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kWillLeaveFullscreenNotification
+ object:nil];
+ DCHECK(isFullscreen_);
+ [self cleanup];
+ isFullscreen_ = NO;
+}
+
+- (void)windowDidChangeScreen:(NSNotification*)notification {
+ [browserController_ resizeFullscreenWindow];
+}
+
+- (void)windowDidMove:(NSNotification*)notification {
+ [browserController_ resizeFullscreenWindow];
+}
+
+- (void)windowDidBecomeMain:(NSNotification*)notification {
+ [self showActiveWindowUI];
+}
+
+- (void)windowDidResignMain:(NSNotification*)notification {
+ [self hideActiveWindowUI];
+}
+
+- (CGFloat)floatingBarVerticalOffset {
+ return [self isWindowOnPrimaryScreen] ? kFloatingBarVerticalOffset : 0;
+}
+
+- (void)overlayFrameChanged:(NSRect)frame {
+ if (!isFullscreen_)
+ return;
+
+ // Make sure |trackingAreaBounds_| always reflects either the tracking area or
+ // the desired tracking area.
+ trackingAreaBounds_ = frame;
+ // The tracking area should always be at least the height of activation zone.
+ NSRect contentBounds = [contentView_ bounds];
+ trackingAreaBounds_.origin.y =
+ std::min(trackingAreaBounds_.origin.y,
+ NSMaxY(contentBounds) - kDropdownActivationZoneHeight);
+ trackingAreaBounds_.size.height =
+ NSMaxY(contentBounds) - trackingAreaBounds_.origin.y + 1;
+
+ // If an animation is currently running, do not set up a tracking area now.
+ // Instead, leave it to be created it in |-animationDidEnd:|.
+ if (currentAnimation_)
+ return;
+
+ [self setupTrackingArea];
+}
+
+- (void)ensureOverlayShownWithAnimation:(BOOL)animate delay:(BOOL)delay {
+ if (!isFullscreen_)
+ return;
+
+ if (animate) {
+ if (delay) {
+ [self startShowTimer];
+ } else {
+ [self cancelAllTimers];
+ [self changeOverlayToFraction:1 withAnimation:YES];
+ }
+ } else {
+ DCHECK(!delay);
+ [self cancelAllTimers];
+ [self changeOverlayToFraction:1 withAnimation:NO];
+ }
+}
+
+- (void)ensureOverlayHiddenWithAnimation:(BOOL)animate delay:(BOOL)delay {
+ if (!isFullscreen_)
+ return;
+
+ if (animate) {
+ if (delay) {
+ [self startHideTimer];
+ } else {
+ [self cancelAllTimers];
+ [self changeOverlayToFraction:0 withAnimation:YES];
+ }
+ } else {
+ DCHECK(!delay);
+ [self cancelAllTimers];
+ [self changeOverlayToFraction:0 withAnimation:NO];
+ }
+}
+
+- (void)cancelAnimationAndTimers {
+ [self cancelAllTimers];
+ [currentAnimation_ stopAnimation];
+ currentAnimation_.reset();
+}
+
+- (CGFloat)floatingBarShownFraction {
+ return [browserController_ floatingBarShownFraction];
+}
+
+- (void)changeFloatingBarShownFraction:(CGFloat)fraction {
+ [browserController_ setFloatingBarShownFraction:fraction];
+
+ mac_util::FullScreenMode desiredMode = [self desiredFullscreenMode];
+ if (desiredMode != currentFullscreenMode_ && [self shouldToggleMenuBar]) {
+ if (currentFullscreenMode_ == mac_util::kFullScreenModeNormal)
+ mac_util::RequestFullScreen(desiredMode);
+ else
+ mac_util::SwitchFullScreenModes(currentFullscreenMode_, desiredMode);
+ currentFullscreenMode_ = desiredMode;
+ }
+}
+
+// Used to activate the floating bar in fullscreen mode.
+- (void)mouseEntered:(NSEvent*)event {
+ DCHECK(isFullscreen_);
+
+ // Having gotten a mouse entered, we no longer need to do exit checks.
+ [self cancelMouseExitCheck];
+
+ NSTrackingArea* trackingArea = [event trackingArea];
+ if (trackingArea == trackingArea_) {
+ // The tracking area shouldn't be active during animation.
+ DCHECK(!currentAnimation_);
+ [self scheduleShowForMouse];
+ }
+}
+
+// Used to deactivate the floating bar in fullscreen mode.
+- (void)mouseExited:(NSEvent*)event {
+ DCHECK(isFullscreen_);
+
+ NSTrackingArea* trackingArea = [event trackingArea];
+ if (trackingArea == trackingArea_) {
+ // The tracking area shouldn't be active during animation.
+ DCHECK(!currentAnimation_);
+
+ // We can get a false mouse exit when the menu slides down, so if the mouse
+ // is still actually over the tracking area, we ignore the mouse exit, but
+ // we set up to check the mouse position again after a delay.
+ if ([self mouseInsideTrackingRect]) {
+ [self setupMouseExitCheck];
+ return;
+ }
+
+ [self scheduleHideForMouse];
+ }
+}
+
+- (void)animationDidStop:(NSAnimation*)animation {
+ // Reset the |currentAnimation_| pointer now that the animation is over.
+ currentAnimation_.reset();
+
+ // Invariant says that the tracking area is not installed while animations are
+ // in progress. Ensure this is true.
+ DCHECK(!trackingArea_);
+ [self removeTrackingAreaIfNecessary]; // For paranoia.
+
+ // Don't automatically set up a new tracking area. When explicitly stopped,
+ // either another animation is going to start immediately or the state will be
+ // changed immediately.
+}
+
+- (void)animationDidEnd:(NSAnimation*)animation {
+ [self animationDidStop:animation];
+
+ // |trackingAreaBounds_| contains the correct tracking area bounds, including
+ // |any updates that may have come while the animation was running. Install a
+ // new tracking area with these bounds.
+ [self setupTrackingArea];
+
+ // TODO(viettrungluu): Better would be to check during the animation; doing it
+ // here means that the timing is slightly off.
+ if (![self mouseInsideTrackingRect])
+ [self scheduleHideForMouse];
+}
+
+@end
+
+
+@implementation FullscreenController (PrivateMethods)
+
+- (BOOL)isWindowOnPrimaryScreen {
+ NSScreen* screen = [[browserController_ window] screen];
+ NSScreen* primaryScreen = [[NSScreen screens] objectAtIndex:0];
+ return (screen == primaryScreen);
+}
+
+- (BOOL)shouldToggleMenuBar {
+ return [self isWindowOnPrimaryScreen] &&
+ [[browserController_ window] isMainWindow];
+}
+
+- (mac_util::FullScreenMode)desiredFullscreenMode {
+ if ([browserController_ floatingBarShownFraction] >= 1.0)
+ return mac_util::kFullScreenModeHideDock;
+ return mac_util::kFullScreenModeHideAll;
+}
+
+- (void)changeOverlayToFraction:(CGFloat)fraction
+ withAnimation:(BOOL)animate {
+ // The non-animated case is really simple, so do it and return.
+ if (!animate) {
+ [currentAnimation_ stopAnimation];
+ [self changeFloatingBarShownFraction:fraction];
+ return;
+ }
+
+ // If we're already animating to the given fraction, then there's nothing more
+ // to do.
+ if (currentAnimation_ && [currentAnimation_ endFraction] == fraction)
+ return;
+
+ // In all other cases, we want to cancel any running animation (which may be
+ // to show or to hide).
+ [currentAnimation_ stopAnimation];
+
+ // Now, if it happens to already be in the right state, there's nothing more
+ // to do.
+ if ([browserController_ floatingBarShownFraction] == fraction)
+ return;
+
+ // Create the animation and set it up.
+ currentAnimation_.reset(
+ [[DropdownAnimation alloc] initWithFraction:fraction
+ fullDuration:kDropdownAnimationDuration
+ animationCurve:NSAnimationEaseOut
+ controller:self]);
+ DCHECK(currentAnimation_);
+ [currentAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
+ [currentAnimation_ setDelegate:self];
+
+ // If there is an existing tracking area, remove it. We do not track mouse
+ // movements during animations (see class comment in the header file).
+ [self removeTrackingAreaIfNecessary];
+
+ [currentAnimation_ startAnimation];
+}
+
+- (void)scheduleShowForMouse {
+ [browserController_ lockBarVisibilityForOwner:self
+ withAnimation:YES
+ delay:YES];
+}
+
+- (void)scheduleHideForMouse {
+ [browserController_ releaseBarVisibilityForOwner:self
+ withAnimation:YES
+ delay:YES];
+}
+
+- (void)setupTrackingArea {
+ if (trackingArea_) {
+ // If the tracking rectangle is already |trackingAreaBounds_|, quit early.
+ NSRect oldRect = [trackingArea_ rect];
+ if (NSEqualRects(trackingAreaBounds_, oldRect))
+ return;
+
+ // Otherwise, remove it.
+ [self removeTrackingAreaIfNecessary];
+ }
+
+ // Create and add a new tracking area for |frame|.
+ trackingArea_.reset(
+ [[NSTrackingArea alloc] initWithRect:trackingAreaBounds_
+ options:NSTrackingMouseEnteredAndExited |
+ NSTrackingActiveInKeyWindow
+ owner:self
+ userInfo:nil]);
+ DCHECK(contentView_);
+ [contentView_ addTrackingArea:trackingArea_];
+}
+
+- (void)removeTrackingAreaIfNecessary {
+ if (trackingArea_) {
+ DCHECK(contentView_); // |contentView_| better be valid.
+ [contentView_ removeTrackingArea:trackingArea_];
+ trackingArea_.reset();
+ }
+}
+
+- (BOOL)mouseInsideTrackingRect {
+ NSWindow* window = [browserController_ window];
+ NSPoint mouseLoc = [window mouseLocationOutsideOfEventStream];
+ NSPoint mousePos = [contentView_ convertPoint:mouseLoc fromView:nil];
+ return NSMouseInRect(mousePos, trackingAreaBounds_, [contentView_ isFlipped]);
+}
+
+- (void)setupMouseExitCheck {
+ [self performSelector:@selector(checkForMouseExit)
+ withObject:nil
+ afterDelay:kMouseExitCheckDelay];
+}
+
+- (void)cancelMouseExitCheck {
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(checkForMouseExit) object:nil];
+}
+
+- (void)checkForMouseExit {
+ if ([self mouseInsideTrackingRect])
+ [self setupMouseExitCheck];
+ else
+ [self scheduleHideForMouse];
+}
+
+- (void)startShowTimer {
+ // If there's already a show timer going, just keep it.
+ if (showTimer_) {
+ DCHECK([showTimer_ isValid]);
+ DCHECK(!hideTimer_);
+ return;
+ }
+
+ // Cancel the hide timer (if necessary) and set up the new show timer.
+ [self cancelHideTimer];
+ showTimer_.reset(
+ [[NSTimer scheduledTimerWithTimeInterval:kDropdownShowDelay
+ target:self
+ selector:@selector(showTimerFire:)
+ userInfo:nil
+ repeats:NO] retain]);
+ DCHECK([showTimer_ isValid]); // This also checks that |showTimer_ != nil|.
+}
+
+- (void)startHideTimer {
+ // If there's already a hide timer going, just keep it.
+ if (hideTimer_) {
+ DCHECK([hideTimer_ isValid]);
+ DCHECK(!showTimer_);
+ return;
+ }
+
+ // Cancel the show timer (if necessary) and set up the new hide timer.
+ [self cancelShowTimer];
+ hideTimer_.reset(
+ [[NSTimer scheduledTimerWithTimeInterval:kDropdownHideDelay
+ target:self
+ selector:@selector(hideTimerFire:)
+ userInfo:nil
+ repeats:NO] retain]);
+ DCHECK([hideTimer_ isValid]); // This also checks that |hideTimer_ != nil|.
+}
+
+- (void)cancelShowTimer {
+ [showTimer_ invalidate];
+ showTimer_.reset();
+}
+
+- (void)cancelHideTimer {
+ [hideTimer_ invalidate];
+ hideTimer_.reset();
+}
+
+- (void)cancelAllTimers {
+ [self cancelShowTimer];
+ [self cancelHideTimer];
+}
+
+- (void)showTimerFire:(NSTimer*)timer {
+ DCHECK_EQ(showTimer_, timer); // This better be our show timer.
+ [showTimer_ invalidate]; // Make sure it doesn't repeat.
+ showTimer_.reset(); // And get rid of it.
+ [self changeOverlayToFraction:1 withAnimation:YES];
+}
+
+- (void)hideTimerFire:(NSTimer*)timer {
+ DCHECK_EQ(hideTimer_, timer); // This better be our hide timer.
+ [hideTimer_ invalidate]; // Make sure it doesn't repeat.
+ hideTimer_.reset(); // And get rid of it.
+ [self changeOverlayToFraction:0 withAnimation:YES];
+}
+
+- (void)cleanup {
+ [self cancelMouseExitCheck];
+ [self cancelAnimationAndTimers];
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+
+ [self removeTrackingAreaIfNecessary];
+ contentView_ = nil;
+
+ // This isn't tracked when not in fullscreen mode.
+ [browserController_ releaseBarVisibilityForOwner:self
+ withAnimation:NO
+ delay:NO];
+
+ // Call the main status resignation code to perform the associated cleanup,
+ // since we will no longer be receiving actual status resignation
+ // notifications.
+ [self hideActiveWindowUI];
+
+ // No more calls back up to the BWC.
+ browserController_ = nil;
+}
+
+- (void)showActiveWindowUI {
+ DCHECK_EQ(currentFullscreenMode_, mac_util::kFullScreenModeNormal);
+ if (currentFullscreenMode_ != mac_util::kFullScreenModeNormal)
+ return;
+
+ if ([self shouldToggleMenuBar]) {
+ mac_util::FullScreenMode desiredMode = [self desiredFullscreenMode];
+ mac_util::RequestFullScreen(desiredMode);
+ currentFullscreenMode_ = desiredMode;
+ }
+
+ // TODO(rohitrao): Insert the Exit Fullscreen button. http://crbug.com/35956
+}
+
+- (void)hideActiveWindowUI {
+ if (currentFullscreenMode_ != mac_util::kFullScreenModeNormal) {
+ mac_util::ReleaseFullScreen(currentFullscreenMode_);
+ currentFullscreenMode_ = mac_util::kFullScreenModeNormal;
+ }
+
+ // TODO(rohitrao): Remove the Exit Fullscreen button. http://crbug.com/35956
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/fullscreen_window.h b/chrome/browser/ui/cocoa/fullscreen_window.h
new file mode 100644
index 0000000..12be00d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/fullscreen_window.h
@@ -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.
+
+#include <Cocoa/Cocoa.h>
+#import "chrome/browser/ui/cocoa/chrome_browser_window.h"
+
+// A FullscreenWindow is a borderless window suitable for going fullscreen. The
+// returned window is NOT release when closed and is not initially visible.
+// FullscreenWindow derives from ChromeBrowserWindow to inherit hole punching,
+// theming methods, and special event handling
+// (e.g. handleExtraKeyboardShortcut).
+@interface FullscreenWindow : ChromeBrowserWindow
+
+// Initialize a FullscreenWindow for the given screen.
+// Designated initializer.
+- (id)initForScreen:(NSScreen*)screen;
+
+@end
diff --git a/chrome/browser/ui/cocoa/fullscreen_window.mm b/chrome/browser/ui/cocoa/fullscreen_window.mm
new file mode 100644
index 0000000..ecbb34c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/fullscreen_window.mm
@@ -0,0 +1,100 @@
+// 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/ui/cocoa/fullscreen_window.h"
+
+#include "base/mac_util.h"
+#include "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+
+@implementation FullscreenWindow
+
+// Make sure our designated initializer gets called.
+- (id)init {
+ return [self initForScreen:[NSScreen mainScreen]];
+}
+
+- (id)initForScreen:(NSScreen*)screen {
+ NSRect contentRect;
+ contentRect.origin = NSZeroPoint;
+ contentRect.size = [screen frame].size;
+
+ if ((self = [super initWithContentRect:contentRect
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:YES
+ screen:screen])) {
+ [self setReleasedWhenClosed:NO];
+ // Borderless windows don't usually show up in the Windows menu so whine at
+ // Cocoa until it complies. See -dealloc and -setTitle: as well.
+ [NSApp addWindowsItem:self title:@"" filename:NO];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ // Paranoia; doesn't seem to be necessary but it doesn't hurt.
+ [NSApp removeWindowsItem:self];
+
+ [super dealloc];
+}
+
+- (void)setTitle:(NSString *)title {
+ [NSApp changeWindowsItem:self title:title filename:NO];
+ [super setTitle:title];
+}
+
+// According to
+// http://www.cocoabuilder.com/archive/message/cocoa/2006/6/19/165953 ,
+// NSBorderlessWindowMask windows cannot become key or main.
+// In our case, however, we don't want that behavior, so we override
+// canBecomeKeyWindow and canBecomeMainWindow.
+
+- (BOOL)canBecomeKeyWindow {
+ return YES;
+}
+
+- (BOOL)canBecomeMainWindow {
+ return YES;
+}
+
+// When becoming/resigning main status, explicitly set the background color,
+// which is required by |TabView|.
+- (void)becomeMainWindow {
+ [super becomeMainWindow];
+ [self setBackgroundColor:[NSColor windowFrameColor]];
+}
+
+- (void)resignMainWindow {
+ [super resignMainWindow];
+ [self setBackgroundColor:[NSColor windowBackgroundColor]];
+}
+
+// We need our own version, since the default one wants to flash the close
+// button (and possibly other things), which results in nothing happening.
+- (void)performClose:(id)sender {
+ BOOL shouldClose = YES;
+
+ // If applicable, check if this window should close.
+ id delegate = [self delegate];
+ if ([delegate respondsToSelector:@selector(windowShouldClose:)])
+ shouldClose = [delegate windowShouldClose:self];
+
+ if (shouldClose) {
+ [self close];
+ }
+}
+
+- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
+ SEL action = [item action];
+
+ // Explicitly enable |-performClose:| (see above); otherwise the fact that
+ // this window does not have a close button results in it being disabled.
+ if (action == @selector(performClose:))
+ return YES;
+
+ return [super validateUserInterfaceItem:item];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/fullscreen_window_unittest.mm b/chrome/browser/ui/cocoa/fullscreen_window_unittest.mm
new file mode 100644
index 0000000..7e54581
--- /dev/null
+++ b/chrome/browser/ui/cocoa/fullscreen_window_unittest.mm
@@ -0,0 +1,47 @@
+// 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/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/fullscreen_window.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface PerformCloseUIItem : NSObject<NSValidatedUserInterfaceItem>
+@end
+
+@implementation PerformCloseUIItem
+- (SEL)action {
+ return @selector(performClose:);
+}
+
+- (NSInteger)tag {
+ return 0;
+}
+@end
+
+class FullscreenWindowTest : public CocoaTest {
+};
+
+TEST_F(FullscreenWindowTest, Basics) {
+ scoped_nsobject<FullscreenWindow> window;
+ window.reset([[FullscreenWindow alloc] init]);
+
+ EXPECT_EQ([NSScreen mainScreen], [window screen]);
+ EXPECT_TRUE([window canBecomeKeyWindow]);
+ EXPECT_TRUE([window canBecomeMainWindow]);
+ EXPECT_EQ(NSBorderlessWindowMask, [window styleMask]);
+ EXPECT_TRUE(NSEqualRects([[NSScreen mainScreen] frame], [window frame]));
+ EXPECT_FALSE([window isReleasedWhenClosed]);
+}
+
+TEST_F(FullscreenWindowTest, CanPerformClose) {
+ scoped_nsobject<FullscreenWindow> window;
+ window.reset([[FullscreenWindow alloc] init]);
+
+ scoped_nsobject<PerformCloseUIItem> item;
+ item.reset([[PerformCloseUIItem alloc] init]);
+
+ EXPECT_TRUE([window validateUserInterfaceItem:item.get()]);
+}
diff --git a/chrome/browser/ui/cocoa/gradient_button_cell.h b/chrome/browser/ui/cocoa/gradient_button_cell.h
new file mode 100644
index 0000000..a1e905f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/gradient_button_cell.h
@@ -0,0 +1,120 @@
+// 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_UI_COCOA_GRADIENT_BUTTON_CELL_H_
+#define CHROME_BROWSER_UI_COCOA_GRADIENT_BUTTON_CELL_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+
+class ThemeProvider;
+
+// Base class for button cells for toolbar and bookmark bar.
+//
+// This is a button cell that handles drawing/highlighting of buttons.
+// The appearance is determined by setting the cell's tag (not the
+// view's) to one of the constants below (ButtonType).
+
+// Set this as the cell's tag.
+enum {
+ kLeftButtonType = -1,
+ kLeftButtonWithShadowType = -2,
+ kStandardButtonType = 0,
+ kRightButtonType = 1,
+ kMiddleButtonType = 2,
+ // Draws like a standard button, except when clicked where the interior
+ // doesn't darken using the theme's "pressed" gradient. Instead uses the
+ // normal un-pressed gradient.
+ kStandardButtonTypeWithLimitedClickFeedback = 3,
+};
+typedef NSInteger ButtonType;
+
+namespace gradient_button_cell {
+
+// Pulsing state for this button.
+typedef enum {
+ // Stable states.
+ kPulsedOn,
+ kPulsedOff,
+ // In motion which will end in a stable state.
+ kPulsingOn,
+ kPulsingOff,
+ // In continuous motion.
+ kPulsingContinuous,
+} PulseState;
+
+};
+
+
+@interface GradientButtonCell : NSButtonCell {
+ @private
+ // Custom drawing means we need to perform our own mouse tracking if
+ // the cell is setShowsBorderOnlyWhileMouseInside:YES.
+ BOOL isMouseInside_;
+ scoped_nsobject<NSTrackingArea> trackingArea_;
+ BOOL shouldTheme_;
+ CGFloat hoverAlpha_; // 0-1. Controls the alpha during mouse hover
+ NSTimeInterval lastHoverUpdate_;
+ scoped_nsobject<NSGradient> gradient_;
+ gradient_button_cell::PulseState pulseState_;
+ CGFloat pulseMultiplier_; // for selecting pulse direction when continuous.
+ CGFloat outerStrokeAlphaMult_; // For pulsing.
+ scoped_nsobject<NSImage> overlayImage_;
+}
+
+// Turn off theming. Temporary work-around.
+- (void)setShouldTheme:(BOOL)shouldTheme;
+
+- (void)drawBorderAndFillForTheme:(ThemeProvider*)themeProvider
+ controlView:(NSView*)controlView
+ innerPath:(NSBezierPath*)innerPath
+ showClickedGradient:(BOOL)showClickedGradient
+ showHighlightGradient:(BOOL)showHighlightGradient
+ hoverAlpha:(CGFloat)hoverAlpha
+ active:(BOOL)active
+ cellFrame:(NSRect)cellFrame
+ defaultGradient:(NSGradient*)defaultGradient;
+
+// Let the view know when the mouse moves in and out. A timer will update
+// the current hoverAlpha_ based on these events.
+- (void)setMouseInside:(BOOL)flag animate:(BOOL)animate;
+
+// Gets the path which tightly bounds the outside of the button. This is needed
+// to produce images of clear buttons which only include the area inside, since
+// the background of the button is drawn by someone else.
+- (NSBezierPath*)clipPathForFrame:(NSRect)cellFrame
+ inView:(NSView*)controlView;
+
+// Turn on or off continuous pulsing. When turning off continuous
+// pulsing, leave our pulse state in the correct ending position for
+// our isMouseInside_ property. Public since it's called from the
+// bookmark bubble.
+- (void)setIsContinuousPulsing:(BOOL)continuous;
+
+// Returns continuous pulse state.
+- (BOOL)isContinuousPulsing;
+
+// Safely stop continuous pulsing by turning off all timers.
+// May leave the cell in an odd state.
+// Needed by an owning control's dealloc routine.
+- (void)safelyStopPulsing;
+
+@property(assign, nonatomic) CGFloat hoverAlpha;
+
+// An image that will be drawn after the normal content of the button cell,
+// overlaying it. Never themed.
+@property(retain, nonatomic) NSImage* overlayImage;
+
+@end
+
+@interface GradientButtonCell(TestingAPI)
+- (BOOL)isMouseInside;
+- (BOOL)pulsing;
+- (gradient_button_cell::PulseState)pulseState;
+- (void)setPulseState:(gradient_button_cell::PulseState)pstate;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_GRADIENT_BUTTON_CELL_H_
diff --git a/chrome/browser/ui/cocoa/gradient_button_cell.mm b/chrome/browser/ui/cocoa/gradient_button_cell.mm
new file mode 100644
index 0000000..205e139
--- /dev/null
+++ b/chrome/browser/ui/cocoa/gradient_button_cell.mm
@@ -0,0 +1,719 @@
+// 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 "chrome/browser/ui/cocoa/gradient_button_cell.h"
+
+#include "base/logging.h"
+#import "base/scoped_nsobject.h"
+#import "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/image_utils.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#include "grit/theme_resources.h"
+#import "third_party/GTM/AppKit/GTMNSColor+Luminance.h"
+
+@interface GradientButtonCell (Private)
+- (void)sharedInit;
+
+// Get drawing parameters for a given cell frame in a given view. The inner
+// frame is the one required by |-drawInteriorWithFrame:inView:|. The inner and
+// outer paths are the ones required by |-drawBorderAndFillForTheme:...|. The
+// outer path also gives the area in which to clip. Any of the |return...|
+// arguments may be NULL (in which case the given parameter won't be returned).
+// If |returnInnerPath| or |returnOuterPath|, |*returnInnerPath| or
+// |*returnOuterPath| should be nil, respectively.
+- (void)getDrawParamsForFrame:(NSRect)cellFrame
+ inView:(NSView*)controlView
+ innerFrame:(NSRect*)returnInnerFrame
+ innerPath:(NSBezierPath**)returnInnerPath
+ clipPath:(NSBezierPath**)returnClipPath;
+
+
+@end
+
+
+static const NSTimeInterval kAnimationShowDuration = 0.2;
+
+// Note: due to a bug (?), drawWithFrame:inView: does not call
+// drawBorderAndFillForTheme::::: unless the mouse is inside. The net
+// effect is that our "fade out" when the mouse leaves becaumes
+// instantaneous. When I "fixed" it things looked horrible; the
+// hover-overed bookmark button would stay highlit for 0.4 seconds
+// which felt like latency/lag. I'm leaving the "bug" in place for
+// now so we don't suck. -jrg
+static const NSTimeInterval kAnimationHideDuration = 0.4;
+
+static const NSTimeInterval kAnimationContinuousCycleDuration = 0.4;
+
+@implementation GradientButtonCell
+
+@synthesize hoverAlpha = hoverAlpha_;
+
+// For nib instantiations
+- (id)initWithCoder:(NSCoder*)decoder {
+ if ((self = [super initWithCoder:decoder])) {
+ [self sharedInit];
+ }
+ return self;
+}
+
+// For programmatic instantiations
+- (id)initTextCell:(NSString*)string {
+ if ((self = [super initTextCell:string])) {
+ [self sharedInit];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ if (trackingArea_) {
+ [[self controlView] removeTrackingArea:trackingArea_];
+ trackingArea_.reset();
+ }
+ [super dealloc];
+}
+
+// Return YES if we are pulsing (towards another state or continuously).
+- (BOOL)pulsing {
+ if ((pulseState_ == gradient_button_cell::kPulsingOn) ||
+ (pulseState_ == gradient_button_cell::kPulsingOff) ||
+ (pulseState_ == gradient_button_cell::kPulsingContinuous))
+ return YES;
+ return NO;
+}
+
+// Perform one pulse step when animating a pulse.
+- (void)performOnePulseStep {
+ NSTimeInterval thisUpdate = [NSDate timeIntervalSinceReferenceDate];
+ NSTimeInterval elapsed = thisUpdate - lastHoverUpdate_;
+ CGFloat opacity = [self hoverAlpha];
+
+ // Update opacity based on state.
+ // Adjust state if we have finished.
+ switch (pulseState_) {
+ case gradient_button_cell::kPulsingOn:
+ opacity += elapsed / kAnimationShowDuration;
+ if (opacity > 1.0) {
+ [self setPulseState:gradient_button_cell::kPulsedOn];
+ return;
+ }
+ break;
+ case gradient_button_cell::kPulsingOff:
+ opacity -= elapsed / kAnimationHideDuration;
+ if (opacity < 0.0) {
+ [self setPulseState:gradient_button_cell::kPulsedOff];
+ return;
+ }
+ break;
+ case gradient_button_cell::kPulsingContinuous:
+ opacity += elapsed / kAnimationContinuousCycleDuration * pulseMultiplier_;
+ if (opacity > 1.0) {
+ opacity = 1.0;
+ pulseMultiplier_ *= -1.0;
+ } else if (opacity < 0.0) {
+ opacity = 0.0;
+ pulseMultiplier_ *= -1.0;
+ }
+ outerStrokeAlphaMult_ = opacity;
+ break;
+ default:
+ NOTREACHED() << "unknown pulse state";
+ }
+
+ // Update our control.
+ lastHoverUpdate_ = thisUpdate;
+ [self setHoverAlpha:opacity];
+ [[self controlView] setNeedsDisplay:YES];
+
+ // If our state needs it, keep going.
+ if ([self pulsing]) {
+ [self performSelector:_cmd withObject:nil afterDelay:0.02];
+ }
+}
+
+- (gradient_button_cell::PulseState)pulseState {
+ return pulseState_;
+}
+
+// Set the pulsing state. This can either set the pulse to on or off
+// immediately (e.g. kPulsedOn, kPulsedOff) or initiate an animated
+// state change.
+- (void)setPulseState:(gradient_button_cell::PulseState)pstate {
+ pulseState_ = pstate;
+ pulseMultiplier_ = 0.0;
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+ lastHoverUpdate_ = [NSDate timeIntervalSinceReferenceDate];
+
+ switch (pstate) {
+ case gradient_button_cell::kPulsedOn:
+ case gradient_button_cell::kPulsedOff:
+ outerStrokeAlphaMult_ = 1.0;
+ [self setHoverAlpha:((pulseState_ == gradient_button_cell::kPulsedOn) ?
+ 1.0 : 0.0)];
+ [[self controlView] setNeedsDisplay:YES];
+ break;
+ case gradient_button_cell::kPulsingOn:
+ case gradient_button_cell::kPulsingOff:
+ outerStrokeAlphaMult_ = 1.0;
+ // Set initial value then engage timer.
+ [self setHoverAlpha:((pulseState_ == gradient_button_cell::kPulsingOn) ?
+ 0.0 : 1.0)];
+ [self performOnePulseStep];
+ break;
+ case gradient_button_cell::kPulsingContinuous:
+ // Semantics of continuous pulsing are that we pulse independent
+ // of mouse position.
+ pulseMultiplier_ = 1.0;
+ [self performOnePulseStep];
+ break;
+ default:
+ CHECK(0);
+ break;
+ }
+}
+
+- (void)safelyStopPulsing {
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+}
+
+- (void)setIsContinuousPulsing:(BOOL)continuous {
+ if (!continuous && pulseState_ != gradient_button_cell::kPulsingContinuous)
+ return;
+ if (continuous) {
+ [self setPulseState:gradient_button_cell::kPulsingContinuous];
+ } else {
+ [self setPulseState:(isMouseInside_ ? gradient_button_cell::kPulsedOn :
+ gradient_button_cell::kPulsedOff)];
+ }
+}
+
+- (BOOL)isContinuousPulsing {
+ return (pulseState_ == gradient_button_cell::kPulsingContinuous) ?
+ YES : NO;
+}
+
+#if 1
+// If we are not continuously pulsing, perform a pulse animation to
+// reflect our new state.
+- (void)setMouseInside:(BOOL)flag animate:(BOOL)animated {
+ isMouseInside_ = flag;
+ if (pulseState_ != gradient_button_cell::kPulsingContinuous) {
+ if (animated) {
+ [self setPulseState:(isMouseInside_ ? gradient_button_cell::kPulsingOn :
+ gradient_button_cell::kPulsingOff)];
+ } else {
+ [self setPulseState:(isMouseInside_ ? gradient_button_cell::kPulsedOn :
+ gradient_button_cell::kPulsedOff)];
+ }
+ }
+}
+#else
+
+- (void)adjustHoverValue {
+ NSTimeInterval thisUpdate = [NSDate timeIntervalSinceReferenceDate];
+
+ NSTimeInterval elapsed = thisUpdate - lastHoverUpdate_;
+
+ CGFloat opacity = [self hoverAlpha];
+ if (isMouseInside_) {
+ opacity += elapsed / kAnimationShowDuration;
+ } else {
+ opacity -= elapsed / kAnimationHideDuration;
+ }
+
+ if (!isMouseInside_ && opacity < 0) {
+ opacity = 0;
+ } else if (isMouseInside_ && opacity > 1) {
+ opacity = 1;
+ } else {
+ [self performSelector:_cmd withObject:nil afterDelay:0.02];
+ }
+ lastHoverUpdate_ = thisUpdate;
+ [self setHoverAlpha:opacity];
+
+ [[self controlView] setNeedsDisplay:YES];
+}
+
+- (void)setMouseInside:(BOOL)flag animate:(BOOL)animated {
+ isMouseInside_ = flag;
+ if (animated) {
+ lastHoverUpdate_ = [NSDate timeIntervalSinceReferenceDate];
+ [self adjustHoverValue];
+ } else {
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+ [self setHoverAlpha:flag ? 1.0 : 0.0];
+ }
+ [[self controlView] setNeedsDisplay:YES];
+}
+
+
+
+#endif
+
+- (NSGradient*)gradientForHoverAlpha:(CGFloat)hoverAlpha
+ isThemed:(BOOL)themed {
+ CGFloat startAlpha = 0.6 + 0.3 * hoverAlpha;
+ CGFloat endAlpha = 0.333 * hoverAlpha;
+
+ if (themed) {
+ startAlpha = 0.2 + 0.35 * hoverAlpha;
+ endAlpha = 0.333 * hoverAlpha;
+ }
+
+ NSColor* startColor =
+ [NSColor colorWithCalibratedWhite:1.0
+ alpha:startAlpha];
+ NSColor* endColor =
+ [NSColor colorWithCalibratedWhite:1.0 - 0.15 * hoverAlpha
+ alpha:endAlpha];
+ NSGradient* gradient = [[NSGradient alloc] initWithColorsAndLocations:
+ startColor, hoverAlpha * 0.33,
+ endColor, 1.0, nil];
+
+ return [gradient autorelease];
+}
+
+- (void)sharedInit {
+ shouldTheme_ = YES;
+ pulseState_ = gradient_button_cell::kPulsedOff;
+ pulseMultiplier_ = 1.0;
+ outerStrokeAlphaMult_ = 1.0;
+ gradient_.reset([[self gradientForHoverAlpha:0.0 isThemed:NO] retain]);
+}
+
+- (void)setShouldTheme:(BOOL)shouldTheme {
+ shouldTheme_ = shouldTheme;
+}
+
+- (NSImage*)overlayImage {
+ return overlayImage_.get();
+}
+
+- (void)setOverlayImage:(NSImage*)image {
+ overlayImage_.reset([image retain]);
+ [[self controlView] setNeedsDisplay:YES];
+}
+
+- (NSBackgroundStyle)interiorBackgroundStyle {
+ // Never lower the interior, since that just leads to a weird shadow which can
+ // often interact badly with the theme.
+ return NSBackgroundStyleRaised;
+}
+
+- (void)mouseEntered:(NSEvent*)theEvent {
+ [self setMouseInside:YES animate:YES];
+}
+
+- (void)mouseExited:(NSEvent*)theEvent {
+ [self setMouseInside:NO animate:YES];
+}
+
+- (BOOL)isMouseInside {
+ return trackingArea_ && isMouseInside_;
+}
+
+// Since we have our own drawWithFrame:, we need to also have our own
+// logic for determining when the mouse is inside for honoring this
+// request.
+- (void)setShowsBorderOnlyWhileMouseInside:(BOOL)showOnly {
+ [super setShowsBorderOnlyWhileMouseInside:showOnly];
+ if (showOnly) {
+ if (trackingArea_.get()) {
+ [self setShowsBorderOnlyWhileMouseInside:NO];
+ [[self controlView] removeTrackingArea:trackingArea_];
+ }
+ trackingArea_.reset([[NSTrackingArea alloc]
+ initWithRect:[[self controlView]
+ bounds]
+ options:(NSTrackingMouseEnteredAndExited |
+ NSTrackingActiveInActiveApp)
+ owner:self
+ userInfo:nil]);
+ [[self controlView] addTrackingArea:trackingArea_];
+ } else {
+ if (trackingArea_) {
+ [[self controlView] removeTrackingArea:trackingArea_];
+ trackingArea_.reset(nil);
+ isMouseInside_ = NO;
+ }
+ }
+}
+
+// TODO(viettrungluu): clean up/reorganize.
+- (void)drawBorderAndFillForTheme:(ThemeProvider*)themeProvider
+ controlView:(NSView*)controlView
+ innerPath:(NSBezierPath*)innerPath
+ showClickedGradient:(BOOL)showClickedGradient
+ showHighlightGradient:(BOOL)showHighlightGradient
+ hoverAlpha:(CGFloat)hoverAlpha
+ active:(BOOL)active
+ cellFrame:(NSRect)cellFrame
+ defaultGradient:(NSGradient*)defaultGradient {
+ BOOL isFlatButton = [self showsBorderOnlyWhileMouseInside];
+
+ // For flat (unbordered when not hovered) buttons, never use the toolbar
+ // button background image, but the modest gradient used for themed buttons.
+ // To make things even more modest, scale the hover alpha down by 40 percent
+ // unless clicked.
+ NSColor* backgroundImageColor;
+ BOOL useThemeGradient;
+ if (isFlatButton) {
+ backgroundImageColor = nil;
+ useThemeGradient = YES;
+ if (!showClickedGradient)
+ hoverAlpha *= 0.6;
+ } else {
+ backgroundImageColor =
+ themeProvider ?
+ themeProvider->GetNSImageColorNamed(IDR_THEME_BUTTON_BACKGROUND,
+ false) :
+ nil;
+ useThemeGradient = backgroundImageColor ? YES : NO;
+ }
+
+ // The basic gradient shown inside; see above.
+ NSGradient* gradient;
+ if (hoverAlpha == 0 && !useThemeGradient) {
+ gradient = defaultGradient ? defaultGradient
+ : gradient_;
+ } else {
+ gradient = [self gradientForHoverAlpha:hoverAlpha
+ isThemed:useThemeGradient];
+ }
+
+ // If we're drawing a background image, show that; else possibly show the
+ // clicked gradient.
+ if (backgroundImageColor) {
+ [backgroundImageColor set];
+ // Set the phase to match window.
+ NSRect trueRect = [controlView convertRect:cellFrame toView:nil];
+ [[NSGraphicsContext currentContext]
+ setPatternPhase:NSMakePoint(NSMinX(trueRect), NSMaxY(trueRect))];
+ [innerPath fill];
+ } else {
+ if (showClickedGradient) {
+ NSGradient* clickedGradient = nil;
+ if (isFlatButton &&
+ [self tag] == kStandardButtonTypeWithLimitedClickFeedback) {
+ clickedGradient = gradient;
+ } else {
+ clickedGradient = themeProvider ? themeProvider->GetNSGradient(
+ active ?
+ BrowserThemeProvider::GRADIENT_TOOLBAR_BUTTON_PRESSED :
+ BrowserThemeProvider::GRADIENT_TOOLBAR_BUTTON_PRESSED_INACTIVE) :
+ nil;
+ }
+ [clickedGradient drawInBezierPath:innerPath angle:90.0];
+ }
+ }
+
+ // Visually indicate unclicked, enabled buttons.
+ if (!showClickedGradient && [self isEnabled]) {
+ [NSGraphicsContext saveGraphicsState];
+ [innerPath addClip];
+
+ // Draw the inner glow.
+ if (hoverAlpha > 0) {
+ [innerPath setLineWidth:2];
+ [[NSColor colorWithCalibratedWhite:1.0 alpha:0.2 * hoverAlpha] setStroke];
+ [innerPath stroke];
+ }
+
+ // Draw the top inner highlight.
+ NSAffineTransform* highlightTransform = [NSAffineTransform transform];
+ [highlightTransform translateXBy:1 yBy:1];
+ scoped_nsobject<NSBezierPath> highlightPath([innerPath copy]);
+ [highlightPath transformUsingAffineTransform:highlightTransform];
+ [[NSColor colorWithCalibratedWhite:1.0 alpha:0.2] setStroke];
+ [highlightPath stroke];
+
+ // Draw the gradient inside.
+ [gradient drawInBezierPath:innerPath angle:90.0];
+
+ [NSGraphicsContext restoreGraphicsState];
+ }
+
+ // Don't draw anything else for disabled flat buttons.
+ if (isFlatButton && ![self isEnabled])
+ return;
+
+ // Draw the outer stroke.
+ NSColor* strokeColor = nil;
+ if (showClickedGradient) {
+ strokeColor = [NSColor
+ colorWithCalibratedWhite:0.0
+ alpha:0.3 * outerStrokeAlphaMult_];
+ } else {
+ strokeColor = themeProvider ? themeProvider->GetNSColor(
+ active ? BrowserThemeProvider::COLOR_TOOLBAR_BUTTON_STROKE :
+ BrowserThemeProvider::COLOR_TOOLBAR_BUTTON_STROKE_INACTIVE,
+ true) : [NSColor colorWithCalibratedWhite:0.0
+ alpha:0.3 * outerStrokeAlphaMult_];
+ }
+ [strokeColor setStroke];
+
+ [innerPath setLineWidth:1];
+ [innerPath stroke];
+}
+
+// TODO(viettrungluu): clean this up.
+// (Private)
+- (void)getDrawParamsForFrame:(NSRect)cellFrame
+ inView:(NSView*)controlView
+ innerFrame:(NSRect*)returnInnerFrame
+ innerPath:(NSBezierPath**)returnInnerPath
+ clipPath:(NSBezierPath**)returnClipPath {
+ // Constants from Cole. Will kConstant them once the feedback loop
+ // is complete.
+ NSRect drawFrame = NSInsetRect(cellFrame, 1.5, 1.5);
+ NSRect innerFrame = NSInsetRect(cellFrame, 2, 1);
+ const CGFloat radius = 3.5;
+
+ ButtonType type = [[(NSControl*)controlView cell] tag];
+ switch (type) {
+ case kMiddleButtonType:
+ drawFrame.size.width += 20;
+ innerFrame.size.width += 2;
+ // Fallthrough
+ case kRightButtonType:
+ drawFrame.origin.x -= 20;
+ innerFrame.origin.x -= 2;
+ // Fallthrough
+ case kLeftButtonType:
+ case kLeftButtonWithShadowType:
+ drawFrame.size.width += 20;
+ innerFrame.size.width += 2;
+ default:
+ break;
+ }
+ if (type == kLeftButtonWithShadowType)
+ innerFrame.size.width -= 1.0;
+
+ // Return results if |return...| not null.
+ if (returnInnerFrame)
+ *returnInnerFrame = innerFrame;
+ if (returnInnerPath) {
+ DCHECK(*returnInnerPath == nil);
+ *returnInnerPath = [NSBezierPath bezierPathWithRoundedRect:drawFrame
+ xRadius:radius
+ yRadius:radius];
+ }
+ if (returnClipPath) {
+ DCHECK(*returnClipPath == nil);
+ NSRect clipPathRect = NSInsetRect(drawFrame, -0.5, -0.5);
+ *returnClipPath = [NSBezierPath bezierPathWithRoundedRect:clipPathRect
+ xRadius:radius + 0.5
+ yRadius:radius + 0.5];
+ }
+}
+
+// TODO(viettrungluu): clean this up.
+- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
+ NSRect innerFrame;
+ NSBezierPath* innerPath = nil;
+ [self getDrawParamsForFrame:cellFrame
+ inView:controlView
+ innerFrame:&innerFrame
+ innerPath:&innerPath
+ clipPath:NULL];
+
+ BOOL pressed = ([((NSControl*)[self controlView]) isEnabled] &&
+ [self isHighlighted]);
+ NSWindow* window = [controlView window];
+ ThemeProvider* themeProvider = [window themeProvider];
+ BOOL active = [window isKeyWindow] || [window isMainWindow];
+
+ // Stroke the borders and appropriate fill gradient. If we're borderless, the
+ // only time we want to draw the inner gradient is if we're highlighted or if
+ // we're the first responder (when "Full Keyboard Access" is turned on).
+ if (([self isBordered] && ![self showsBorderOnlyWhileMouseInside]) ||
+ pressed ||
+ [self isMouseInside] ||
+ [self isContinuousPulsing] ||
+ [self showsFirstResponder]) {
+
+ // When pulsing we want the bookmark to stand out a little more.
+ BOOL showClickedGradient = pressed ||
+ (pulseState_ == gradient_button_cell::kPulsingContinuous);
+
+ // When first responder, turn the hover alpha all the way up.
+ CGFloat hoverAlpha = [self hoverAlpha];
+ if ([self showsFirstResponder])
+ hoverAlpha = 1.0;
+
+ [self drawBorderAndFillForTheme:themeProvider
+ controlView:controlView
+ innerPath:innerPath
+ showClickedGradient:showClickedGradient
+ showHighlightGradient:[self isHighlighted]
+ hoverAlpha:hoverAlpha
+ active:active
+ cellFrame:cellFrame
+ defaultGradient:nil];
+ }
+
+ // If this is the left side of a segmented button, draw a slight shadow.
+ ButtonType type = [[(NSControl*)controlView cell] tag];
+ if (type == kLeftButtonWithShadowType) {
+ NSRect borderRect, contentRect;
+ NSDivideRect(cellFrame, &borderRect, &contentRect, 1.0, NSMaxXEdge);
+ NSColor* stroke = themeProvider ? themeProvider->GetNSColor(
+ active ? BrowserThemeProvider::COLOR_TOOLBAR_BUTTON_STROKE :
+ BrowserThemeProvider::COLOR_TOOLBAR_BUTTON_STROKE_INACTIVE,
+ true) : [NSColor blackColor];
+
+ [[stroke colorWithAlphaComponent:0.2] set];
+ NSRectFillUsingOperation(NSInsetRect(borderRect, 0, 2),
+ NSCompositeSourceOver);
+ }
+ [self drawInteriorWithFrame:innerFrame inView:controlView];
+}
+
+- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
+ if (shouldTheme_) {
+ BOOL isTemplate = [[self image] isTemplate];
+
+ [NSGraphicsContext saveGraphicsState];
+
+ CGContextRef context =
+ (CGContextRef)([[NSGraphicsContext currentContext] graphicsPort]);
+
+ BrowserThemeProvider* themeProvider = static_cast<BrowserThemeProvider*>(
+ [[controlView window] themeProvider]);
+ NSColor* color = themeProvider ?
+ themeProvider->GetNSColorTint(BrowserThemeProvider::TINT_BUTTONS,
+ true) :
+ [NSColor blackColor];
+
+ if (isTemplate && themeProvider && themeProvider->UsingDefaultTheme()) {
+ scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
+ [shadow.get() setShadowColor:themeProvider->GetNSColor(
+ BrowserThemeProvider::COLOR_TOOLBAR_BEZEL, true)];
+ [shadow.get() setShadowOffset:NSMakeSize(0.0, -1.0)];
+ [shadow setShadowBlurRadius:1.0];
+ [shadow set];
+ }
+
+ CGContextBeginTransparencyLayer(context, 0);
+ NSRect imageRect = NSZeroRect;
+ imageRect.size = [[self image] size];
+ NSRect drawRect = [self imageRectForBounds:cellFrame];
+ [[self image] drawInRect:drawRect
+ fromRect:imageRect
+ operation:NSCompositeSourceOver
+ fraction:[self isEnabled] ? 1.0 : 0.5
+ neverFlipped:YES];
+ if (isTemplate && color) {
+ [color set];
+ NSRectFillUsingOperation(cellFrame, NSCompositeSourceAtop);
+ }
+ CGContextEndTransparencyLayer(context);
+
+ [NSGraphicsContext restoreGraphicsState];
+ } else {
+ // NSCell draws these off-center for some reason, probably because of the
+ // positioning of the control in the xib.
+ [super drawInteriorWithFrame:NSOffsetRect(cellFrame, 0, 1)
+ inView:controlView];
+ }
+
+ if (overlayImage_) {
+ NSRect imageRect = NSZeroRect;
+ imageRect.size = [overlayImage_ size];
+ [overlayImage_ drawInRect:[self imageRectForBounds:cellFrame]
+ fromRect:imageRect
+ operation:NSCompositeSourceOver
+ fraction:[self isEnabled] ? 1.0 : 0.5
+ neverFlipped:YES];
+ }
+}
+
+// Overriden from NSButtonCell so we can display a nice fadeout effect for
+// button titles that overflow.
+// This method is copied in the most part from GTMFadeTruncatingTextFieldCell,
+// the only difference is that here we draw the text ourselves rather than
+// calling the super to do the work.
+// We can't use GTMFadeTruncatingTextFieldCell because there's no easy way to
+// get it to work with NSButtonCell.
+// TODO(jeremy): Move this to GTM.
+- (NSRect)drawTitle:(NSAttributedString *)title
+ withFrame:(NSRect)cellFrame
+ inView:(NSView *)controlView {
+ NSSize size = [title size];
+
+ // Empirically, Cocoa will draw an extra 2 pixels past NSWidth(cellFrame)
+ // before it clips the text.
+ const CGFloat kOverflowBeforeClip = 2;
+ // Don't complicate drawing unless we need to clip.
+ if (floor(size.width) <= (NSWidth(cellFrame) + kOverflowBeforeClip)) {
+ return [super drawTitle:title withFrame:cellFrame inView:controlView];
+ }
+
+ // Gradient is about twice our line height long.
+ CGFloat gradientWidth = MIN(size.height * 2, NSWidth(cellFrame) / 4);
+
+ NSRect solidPart, gradientPart;
+ NSDivideRect(cellFrame, &gradientPart, &solidPart, gradientWidth, NSMaxXEdge);
+
+ // Draw non-gradient part without transparency layer, as light text on a dark
+ // background looks bad with a gradient layer.
+ [[NSGraphicsContext currentContext] saveGraphicsState];
+ [NSBezierPath clipRect:solidPart];
+
+ // 11 is the magic number needed to make this match the native NSButtonCell's
+ // label display.
+ CGFloat textLeft = [[self image] size].width + 11;
+
+ // For some reason, the height of cellFrame as passed in is totally bogus.
+ // For vertical centering purposes, we need the bounds of the containing
+ // view.
+ NSRect buttonFrame = [[self controlView] frame];
+
+ // Off-by-one to match native NSButtonCell's version.
+ NSPoint textOffset = NSMakePoint(textLeft,
+ (NSHeight(buttonFrame) - size.height)/2 + 1);
+ [title drawAtPoint:textOffset];
+ [[NSGraphicsContext currentContext] restoreGraphicsState];
+
+ // Draw the gradient part with a transparency layer. This makes the text look
+ // suboptimal, but since it fades out, that's ok.
+ [[NSGraphicsContext currentContext] saveGraphicsState];
+ [NSBezierPath clipRect:gradientPart];
+ CGContextRef context = static_cast<CGContextRef>(
+ [[NSGraphicsContext currentContext] graphicsPort]);
+ CGContextBeginTransparencyLayerWithRect(context,
+ NSRectToCGRect(gradientPart), 0);
+ [title drawAtPoint:textOffset];
+
+ // TODO(alcor): switch this to GTMLinearRGBShading if we ever need on 10.4
+ NSColor *color = [NSColor textColor]; //[self textColor];
+ NSColor *alphaColor = [color colorWithAlphaComponent:0.0];
+ NSGradient *mask = [[NSGradient alloc] initWithStartingColor:color
+ endingColor:alphaColor];
+
+ // Draw the gradient mask
+ CGContextSetBlendMode(context, kCGBlendModeDestinationIn);
+ [mask drawFromPoint:NSMakePoint(NSMaxX(cellFrame) - gradientWidth,
+ NSMinY(cellFrame))
+ toPoint:NSMakePoint(NSMaxX(cellFrame),
+ NSMinY(cellFrame))
+ options:NSGradientDrawsBeforeStartingLocation];
+ [mask release];
+ CGContextEndTransparencyLayer(context);
+ [[NSGraphicsContext currentContext] restoreGraphicsState];
+
+ return cellFrame;
+}
+
+- (NSBezierPath*)clipPathForFrame:(NSRect)cellFrame
+ inView:(NSView*)controlView {
+ NSBezierPath* boundingPath = nil;
+ [self getDrawParamsForFrame:cellFrame
+ inView:controlView
+ innerFrame:NULL
+ innerPath:NULL
+ clipPath:&boundingPath];
+ return boundingPath;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/gradient_button_cell_unittest.mm b/chrome/browser/ui/cocoa/gradient_button_cell_unittest.mm
new file mode 100644
index 0000000..a9d09da
--- /dev/null
+++ b/chrome/browser/ui/cocoa/gradient_button_cell_unittest.mm
@@ -0,0 +1,112 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/gradient_button_cell.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface GradientButtonCell (HoverValueTesting)
+- (void)performOnePulseStep;
+@end
+
+namespace {
+
+class GradientButtonCellTest : public CocoaTest {
+ public:
+ GradientButtonCellTest() {
+ NSRect frame = NSMakeRect(0, 0, 50, 30);
+ scoped_nsobject<NSButton>view([[NSButton alloc] initWithFrame:frame]);
+ view_ = view.get();
+ scoped_nsobject<GradientButtonCell> cell([[GradientButtonCell alloc]
+ initTextCell:@"Testing"]);
+ [view_ setCell:cell.get()];
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ NSButton* view_;
+};
+
+TEST_VIEW(GradientButtonCellTest, view_)
+
+// Test drawing, mostly to ensure nothing leaks or crashes.
+TEST_F(GradientButtonCellTest, DisplayWithHover) {
+ [[view_ cell] setHoverAlpha:0.0];
+ [view_ display];
+ [[view_ cell] setHoverAlpha:0.5];
+ [view_ display];
+ [[view_ cell] setHoverAlpha:1.0];
+ [view_ display];
+}
+
+// Test hover, mostly to ensure nothing leaks or crashes.
+TEST_F(GradientButtonCellTest, Hover) {
+ GradientButtonCell* cell = [view_ cell];
+ [cell setMouseInside:YES animate:NO];
+ EXPECT_EQ([[view_ cell] hoverAlpha], 1.0);
+
+ [cell setMouseInside:NO animate:YES];
+ CGFloat alpha1 = [cell hoverAlpha];
+ [cell performOnePulseStep];
+ CGFloat alpha2 = [cell hoverAlpha];
+ EXPECT_TRUE(alpha2 < alpha1);
+}
+
+// Tracking rects
+TEST_F(GradientButtonCellTest, TrackingRects) {
+ GradientButtonCell* cell = [view_ cell];
+ EXPECT_FALSE([cell showsBorderOnlyWhileMouseInside]);
+ EXPECT_FALSE([cell isMouseInside]);
+
+ [cell setShowsBorderOnlyWhileMouseInside:YES];
+ [cell mouseEntered:nil];
+ EXPECT_TRUE([cell isMouseInside]);
+ [cell mouseExited:nil];
+ EXPECT_FALSE([cell isMouseInside]);
+
+ [cell setShowsBorderOnlyWhileMouseInside:NO];
+ EXPECT_FALSE([cell isMouseInside]);
+
+ [cell setShowsBorderOnlyWhileMouseInside:YES];
+ [cell setShowsBorderOnlyWhileMouseInside:YES];
+ [cell setShowsBorderOnlyWhileMouseInside:NO];
+ [cell setShowsBorderOnlyWhileMouseInside:NO];
+}
+
+TEST_F(GradientButtonCellTest, ContinuousPulseOnOff) {
+ GradientButtonCell* cell = [view_ cell];
+
+ // On/off
+ EXPECT_FALSE([cell isContinuousPulsing]);
+ [cell setIsContinuousPulsing:YES];
+ EXPECT_TRUE([cell isContinuousPulsing]);
+ EXPECT_TRUE([cell pulsing]);
+ [cell setIsContinuousPulsing:NO];
+ EXPECT_FALSE([cell isContinuousPulsing]);
+
+ // On/safeOff
+ [cell setIsContinuousPulsing:YES];
+ EXPECT_TRUE([cell isContinuousPulsing]);
+ [cell safelyStopPulsing];
+}
+
+// More for valgrind; we don't confirm state change does anything useful.
+TEST_F(GradientButtonCellTest, PulseState) {
+ GradientButtonCell* cell = [view_ cell];
+
+ [cell setMouseInside:YES animate:YES];
+ // Allow for immediate state changes to keep test unflaky
+ EXPECT_TRUE(([cell pulseState] == gradient_button_cell::kPulsingOn) ||
+ ([cell pulseState] == gradient_button_cell::kPulsedOn));
+
+ [cell setMouseInside:NO animate:YES];
+ // Allow for immediate state changes to keep test unflaky
+ EXPECT_TRUE(([cell pulseState] == gradient_button_cell::kPulsingOff) ||
+ ([cell pulseState] == gradient_button_cell::kPulsedOff));
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/history_menu_bridge.h b/chrome/browser/ui/cocoa/history_menu_bridge.h
new file mode 100644
index 0000000..db4a37b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/history_menu_bridge.h
@@ -0,0 +1,232 @@
+// 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_UI_COCOA_HISTORY_MENU_BRIDGE_H_
+#define CHROME_BROWSER_UI_COCOA_HISTORY_MENU_BRIDGE_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+#include <map>
+
+#include "base/ref_counted.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/cancelable_request.h"
+#import "chrome/browser/favicon_service.h"
+#include "chrome/browser/history/history.h"
+#include "chrome/browser/sessions/session_id.h"
+#include "chrome/browser/sessions/tab_restore_service.h"
+#include "chrome/browser/sessions/tab_restore_service_observer.h"
+#include "chrome/common/notification_observer.h"
+
+class NavigationEntry;
+class NotificationRegistrar;
+class PageUsageData;
+class Profile;
+class TabNavigationEntry;
+class TabRestoreService;
+@class HistoryMenuCocoaController;
+
+namespace {
+
+class HistoryMenuBridgeTest;
+
+}
+
+// C++ bridge for the history menu; one per AppController (means there
+// is only one). This class observes various data sources, namely the
+// HistoryService and the TabRestoreService, and then updates the NSMenu when
+// there is new data.
+//
+// The history menu is broken up into sections: most visisted and recently
+// closed. The overall menu has a tag of IDC_HISTORY_MENU, with the user content
+// items having the local tags defined in the enum below. Items within a section
+// all share the same tag. The structure of the menu is laid out in MainMenu.xib
+// and the generated content is inserted after the Title elements. The recently
+// closed section is special in that those menu items can have submenus to list
+// all the tabs within that closed window. By convention, these submenu items
+// have a tag that's equal to the parent + 1. Tags within the history menu have
+// a range of [400,500) and do not go through CommandDispatch for their target-
+// action mechanism.
+//
+// These menu items do not use firstResponder as their target. Rather, they are
+// hooked directly up to the HistoryMenuCocoaController that then bridges back
+// to this class. These items are created via the AddItemToMenu() helper. Also,
+// unlike the typical ownership model, this bridge owns its controller. The
+// controller is very thin and only exists to interact with Cocoa, but this
+// class does the bulk of the work.
+class HistoryMenuBridge : public NotificationObserver,
+ public TabRestoreServiceObserver {
+ public:
+ // This is a generalization of the data we store in the history menu because
+ // we pull things from different sources with different data types.
+ struct HistoryItem {
+ public:
+ HistoryItem();
+ // Copy constructor allowed.
+ HistoryItem(const HistoryItem& copy);
+ ~HistoryItem();
+
+ // The title for the menu item.
+ string16 title;
+ // The URL that will be navigated to if the user selects this item.
+ GURL url;
+ // Favicon for the URL.
+ scoped_nsobject<NSImage> icon;
+
+ // If the icon is being requested from the FaviconService, |icon_requested|
+ // will be true and |icon_handle| will be non-NULL. If this is false, then
+ // |icon_handle| will be NULL.
+ bool icon_requested;
+ // The Handle given to us by the FaviconService for the icon fetch request.
+ FaviconService::Handle icon_handle;
+
+ // The pointer to the item after it has been created. Strong; NSMenu also
+ // retains this. During a rebuild flood (if the user closes a lot of tabs
+ // quickly), the NSMenu can release the item before the HistoryItem has
+ // been fully deleted. If this were a weak pointer, it would result in a
+ // zombie.
+ scoped_nsobject<NSMenuItem> menu_item;
+
+ // This ID is unique for a browser session and can be passed to the
+ // TabRestoreService to re-open the closed window or tab that this
+ // references. A non-0 session ID indicates that this is an entry can be
+ // restored that way. Otherwise, the URL will be used to open the item and
+ // this ID will be 0.
+ SessionID::id_type session_id;
+
+ // If the HistoryItem is a window, this will be the vector of tabs. Note
+ // that this is a list of weak references. The |menu_item_map_| is the owner
+ // of all items. If it is not a window, then the entry is a single page and
+ // the vector will be empty.
+ std::vector<HistoryItem*> tabs;
+
+ private:
+ // Copying is explicitly allowed, but assignment is not.
+ void operator=(const HistoryItem&);
+ };
+
+ // These tags are not global view tags and are local to the history menu. The
+ // normal procedure for menu items is to go through CommandDispatch, but since
+ // history menu items are hooked directly up to their target, they do not need
+ // to have the global IDC view tags.
+ enum Tags {
+ kMostVisitedSeparator = 400, // Separator before most visited section.
+ kMostVisitedTitle = 401, // Title of the most visited section.
+ kMostVisited = 420, // Used for all entries in the most visited section.
+ kRecentlyClosedSeparator = 440, // Item before recently closed section.
+ kRecentlyClosedTitle = 441, // Title of recently closed section.
+ kRecentlyClosed = 460, // Used for items in the recently closed section.
+ kShowFullSeparator = 480 // Separator after the recently closed section.
+ };
+
+ explicit HistoryMenuBridge(Profile* profile);
+ virtual ~HistoryMenuBridge();
+
+ // Overriden from NotificationObserver.
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+
+ // For TabRestoreServiceObserver
+ virtual void TabRestoreServiceChanged(TabRestoreService* service);
+ virtual void TabRestoreServiceDestroyed(TabRestoreService* service);
+
+ // Looks up an NSMenuItem in the |menu_item_map_| and returns the
+ // corresponding HistoryItem.
+ HistoryItem* HistoryItemForMenuItem(NSMenuItem* item);
+
+ // I wish I has a "friend @class" construct. These are used by the HMCC
+ // to access model information when responding to actions.
+ HistoryService* service();
+ Profile* profile();
+
+ protected:
+ // Return the History menu.
+ virtual NSMenu* HistoryMenu();
+
+ // Clear items in the given |menu|. Menu items in the same section are given
+ // the same tag. This will go through the entire history menu, removing all
+ // items with a given tag. Note that this will recurse to submenus, removing
+ // child items from the menu item map. This will only remove items that have
+ // a target hooked up to the |controller_|.
+ void ClearMenuSection(NSMenu* menu, NSInteger tag);
+
+ // Adds a given title and URL to the passed-in menu with a certain tag and
+ // index. This will add |item| and the newly created menu item to the
+ // |menu_item_map_|, which takes ownership. Items are deleted in
+ // ClearMenuSection(). This returns the new menu item that was just added.
+ NSMenuItem* AddItemToMenu(HistoryItem* item,
+ NSMenu* menu,
+ NSInteger tag,
+ NSInteger index);
+
+ // Called by the ctor if |service_| is ready at the time, or by a
+ // notification receiver. Finishes initialization tasks by subscribing for
+ // change notifications and calling CreateMenu().
+ void Init();
+
+ // Does the query for the history information to create the menu.
+ void CreateMenu();
+
+ // Callback method for when HistoryService query results are ready with the
+ // most recently-visited sites.
+ void OnVisitedHistoryResults(CancelableRequestProvider::Handle handle,
+ std::vector<PageUsageData*>* results);
+
+ // Creates a HistoryItem* for the given tab entry. Caller takes ownership of
+ // the result and must delete it when finished.
+ HistoryItem* HistoryItemForTab(const TabRestoreService::Tab& entry);
+
+ // Helper function that sends an async request to the FaviconService to get
+ // an icon. The callback will update the NSMenuItem directly.
+ void GetFaviconForHistoryItem(HistoryItem* item);
+
+ // Callback for the FaviconService to return favicon image data when we
+ // request it. This decodes the raw data, updates the HistoryItem, and then
+ // sets the image on the menu. Called on the same same thread that
+ // GetFaviconForHistoryItem() was called on (UI thread).
+ void GotFaviconData(FaviconService::Handle handle,
+ bool know_favicon,
+ scoped_refptr<RefCountedMemory> data,
+ bool expired,
+ GURL url);
+
+ // Cancels a favicon load request for a given HistoryItem, if one is in
+ // progress.
+ void CancelFaviconRequest(HistoryItem* item);
+
+ private:
+ friend class ::HistoryMenuBridgeTest;
+ friend class HistoryMenuCocoaControllerTest;
+
+ scoped_nsobject<HistoryMenuCocoaController> controller_; // strong
+
+ Profile* profile_; // weak
+ HistoryService* history_service_; // weak
+ TabRestoreService* tab_restore_service_; // weak
+
+ NotificationRegistrar registrar_;
+ CancelableRequestConsumer cancelable_request_consumer_;
+
+ // Mapping of NSMenuItems to HistoryItems. This owns the HistoryItems until
+ // they are removed and deleted via ClearMenuSection().
+ std::map<NSMenuItem*, HistoryItem*> menu_item_map_;
+
+ // Maps HistoryItems to favicon request Handles.
+ CancelableRequestConsumerTSimple<HistoryItem*> favicon_consumer_;
+
+ // Requests to re-create the menu are coalesced. |create_in_progress_| is true
+ // when either waiting for the history service to return query results, or
+ // when the menu is rebuilding. |need_recreate_| is true whenever a rebuild
+ // has been scheduled but is waiting for the current one to finish.
+ bool create_in_progress_;
+ bool need_recreate_;
+
+ // The default favicon if a HistoryItem does not have one.
+ scoped_nsobject<NSImage> default_favicon_;
+
+ DISALLOW_COPY_AND_ASSIGN(HistoryMenuBridge);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_HISTORY_MENU_BRIDGE_H_
diff --git a/chrome/browser/ui/cocoa/history_menu_bridge.mm b/chrome/browser/ui/cocoa/history_menu_bridge.mm
new file mode 100644
index 0000000..320632f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/history_menu_bridge.mm
@@ -0,0 +1,470 @@
+// 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 "chrome/browser/ui/cocoa/history_menu_bridge.h"
+
+#include "app/l10n_util.h"
+#include "app/resource_bundle.h"
+#include "base/callback.h"
+#include "base/stl_util-inl.h"
+#include "base/string_number_conversions.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h" // IDC_HISTORY_MENU
+#import "chrome/browser/app_controller_mac.h"
+#include "chrome/browser/history/page_usage_data.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/sessions/session_types.h"
+#import "chrome/browser/ui/cocoa/history_menu_cocoa_controller.h"
+#include "chrome/common/notification_registrar.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/url_constants.h"
+#include "gfx/codec/png_codec.h"
+#include "grit/app_resources.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+namespace {
+
+// Menus more than this many chars long will get trimmed.
+const NSUInteger kMaximumMenuWidthInChars = 50;
+
+// When trimming, use this many chars from each side.
+const NSUInteger kMenuTrimSizeInChars = 25;
+
+// Number of days to consider when getting the number of most visited items.
+const int kMostVisitedScope = 90;
+
+// The number of most visisted results to get.
+const int kMostVisitedCount = 9;
+
+// The number of recently closed items to get.
+const unsigned int kRecentlyClosedCount = 10;
+
+} // namespace
+
+HistoryMenuBridge::HistoryItem::HistoryItem()
+ : icon_requested(false),
+ menu_item(nil),
+ session_id(0) {
+}
+
+HistoryMenuBridge::HistoryItem::HistoryItem(const HistoryItem& copy)
+ : title(copy.title),
+ url(copy.url),
+ icon_requested(false),
+ menu_item(nil),
+ session_id(copy.session_id) {
+}
+
+HistoryMenuBridge::HistoryItem::~HistoryItem() {
+}
+
+HistoryMenuBridge::HistoryMenuBridge(Profile* profile)
+ : controller_([[HistoryMenuCocoaController alloc] initWithBridge:this]),
+ profile_(profile),
+ history_service_(NULL),
+ tab_restore_service_(NULL),
+ create_in_progress_(false),
+ need_recreate_(false) {
+ // If we don't have a profile, do not bother initializing our data sources.
+ // This shouldn't happen except in unit tests.
+ if (profile_) {
+ // Check to see if the history service is ready. Because it loads async, it
+ // may not be ready when the Bridge is created. If this happens, register
+ // for a notification that tells us the HistoryService is ready.
+ HistoryService* hs = profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
+ if (hs != NULL && hs->BackendLoaded()) {
+ history_service_ = hs;
+ Init();
+ }
+
+ // TODO(???): NULL here means we're OTR. Show this in the GUI somehow?
+ tab_restore_service_ = profile_->GetTabRestoreService();
+ if (tab_restore_service_) {
+ tab_restore_service_->AddObserver(this);
+ tab_restore_service_->LoadTabsFromLastSession();
+ }
+ }
+
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ default_favicon_.reset([rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON) retain]);
+
+ // Set the static icons in the menu.
+ NSMenuItem* item = [HistoryMenu() itemWithTag:IDC_SHOW_HISTORY];
+ [item setImage:rb.GetNativeImageNamed(IDR_HISTORY_FAVICON)];
+
+ // The service is not ready for use yet, so become notified when it does.
+ if (!history_service_) {
+ registrar_.Add(this,
+ NotificationType::HISTORY_LOADED,
+ NotificationService::AllSources());
+ }
+}
+
+// Note that all requests sent to either the history service or the favicon
+// service will be automatically cancelled by their respective Consumers, so
+// task cancellation is not done manually here in the dtor.
+HistoryMenuBridge::~HistoryMenuBridge() {
+ // Unregister ourselves as observers and notifications.
+ const NotificationSource& src = NotificationService::AllSources();
+ if (history_service_) {
+ registrar_.Remove(this, NotificationType::HISTORY_TYPED_URLS_MODIFIED, src);
+ registrar_.Remove(this, NotificationType::HISTORY_URL_VISITED, src);
+ registrar_.Remove(this, NotificationType::HISTORY_URLS_DELETED, src);
+ } else {
+ registrar_.Remove(this, NotificationType::HISTORY_LOADED, src);
+ }
+
+ if (tab_restore_service_)
+ tab_restore_service_->RemoveObserver(this);
+
+ // Since the map owns the HistoryItems, delete anything that still exists.
+ std::map<NSMenuItem*, HistoryItem*>::iterator it = menu_item_map_.begin();
+ while (it != menu_item_map_.end()) {
+ HistoryItem* item = it->second;
+ menu_item_map_.erase(it++);
+ delete item;
+ }
+}
+
+void HistoryMenuBridge::Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ // A history service is now ready. Check to see if it's the one for the main
+ // profile. If so, perform final initialization.
+ if (type == NotificationType::HISTORY_LOADED) {
+ HistoryService* hs =
+ profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
+ if (hs != NULL && hs->BackendLoaded()) {
+ history_service_ = hs;
+ Init();
+
+ // Found our HistoryService, so stop listening for this notification.
+ registrar_.Remove(this,
+ NotificationType::HISTORY_LOADED,
+ NotificationService::AllSources());
+ }
+ }
+
+ // All other notification types that we observe indicate that the history has
+ // changed and we need to rebuild.
+ need_recreate_ = true;
+ CreateMenu();
+}
+
+void HistoryMenuBridge::TabRestoreServiceChanged(TabRestoreService* service) {
+ const TabRestoreService::Entries& entries = service->entries();
+
+ // Clear the history menu before rebuilding.
+ NSMenu* menu = HistoryMenu();
+ ClearMenuSection(menu, kRecentlyClosed);
+
+ // Index for the next menu item.
+ NSInteger index = [menu indexOfItemWithTag:kRecentlyClosedTitle] + 1;
+ NSUInteger added_count = 0;
+
+ for (TabRestoreService::Entries::const_iterator it = entries.begin();
+ it != entries.end() && added_count < kRecentlyClosedCount; ++it) {
+ TabRestoreService::Entry* entry = *it;
+
+ // If this is a window, create a submenu for all of its tabs.
+ if (entry->type == TabRestoreService::WINDOW) {
+ TabRestoreService::Window* entry_win = (TabRestoreService::Window*)entry;
+ std::vector<TabRestoreService::Tab>& tabs = entry_win->tabs;
+ if (!tabs.size())
+ continue;
+
+ // Create the item for the parent/window. Do not set the title yet because
+ // the actual number of items that are in the menu will not be known until
+ // things like the NTP are filtered out, which is done when the tab items
+ // are actually created.
+ HistoryItem* item = new HistoryItem();
+ item->session_id = entry_win->id;
+
+ // Create the submenu.
+ scoped_nsobject<NSMenu> submenu([[NSMenu alloc] init]);
+
+ // Create standard items within the window submenu.
+ NSString* restore_title = l10n_util::GetNSString(
+ IDS_HISTORY_CLOSED_RESTORE_WINDOW_MAC);
+ scoped_nsobject<NSMenuItem> restore_item(
+ [[NSMenuItem alloc] initWithTitle:restore_title
+ action:@selector(openHistoryMenuItem:)
+ keyEquivalent:@""]);
+ [restore_item setTarget:controller_.get()];
+ // Duplicate the HistoryItem otherwise the different NSMenuItems will
+ // point to the same HistoryItem, which would then be double-freed when
+ // removing the items from the map or in the dtor.
+ HistoryItem* dup_item = new HistoryItem(*item);
+ menu_item_map_.insert(std::make_pair(restore_item.get(), dup_item));
+ [submenu addItem:restore_item.get()];
+ [submenu addItem:[NSMenuItem separatorItem]];
+
+ // Loop over the window's tabs and add them to the submenu.
+ NSInteger subindex = [[submenu itemArray] count];
+ std::vector<TabRestoreService::Tab>::const_iterator it;
+ for (it = tabs.begin(); it != tabs.end(); ++it) {
+ TabRestoreService::Tab tab = *it;
+ HistoryItem* tab_item = HistoryItemForTab(tab);
+ if (tab_item) {
+ item->tabs.push_back(tab_item);
+ AddItemToMenu(tab_item, submenu.get(), kRecentlyClosed + 1,
+ subindex++);
+ }
+ }
+
+ // Now that the number of tabs that has been added is known, set the title
+ // of the parent menu item.
+ if (item->tabs.size() == 1) {
+ item->title = l10n_util::GetStringUTF16(
+ IDS_NEW_TAB_RECENTLY_CLOSED_WINDOW_SINGLE);
+ } else {
+ item->title =l10n_util::GetStringFUTF16(
+ IDS_NEW_TAB_RECENTLY_CLOSED_WINDOW_MULTIPLE,
+ base::IntToString16(item->tabs.size()));
+ }
+
+ // Sometimes it is possible for there to not be any subitems for a given
+ // window; if that is the case, do not add the entry to the main menu.
+ if ([[submenu itemArray] count] > 2) {
+ // Create the menu item parent.
+ NSMenuItem* parent_item =
+ AddItemToMenu(item, menu, kRecentlyClosed, index++);
+ [parent_item setSubmenu:submenu.get()];
+ ++added_count;
+ }
+ } else if (entry->type == TabRestoreService::TAB) {
+ TabRestoreService::Tab* tab =
+ static_cast<TabRestoreService::Tab*>(entry);
+ HistoryItem* item = HistoryItemForTab(*tab);
+ if (item) {
+ AddItemToMenu(item, menu, kRecentlyClosed, index++);
+ ++added_count;
+ }
+ }
+ }
+}
+
+void HistoryMenuBridge::TabRestoreServiceDestroyed(
+ TabRestoreService* service) {
+ // Intentionally left blank. We hold a weak reference to the service.
+}
+
+HistoryMenuBridge::HistoryItem* HistoryMenuBridge::HistoryItemForMenuItem(
+ NSMenuItem* item) {
+ std::map<NSMenuItem*, HistoryItem*>::iterator it = menu_item_map_.find(item);
+ if (it != menu_item_map_.end()) {
+ return it->second;
+ }
+ return NULL;
+}
+
+HistoryService* HistoryMenuBridge::service() {
+ return history_service_;
+}
+
+Profile* HistoryMenuBridge::profile() {
+ return profile_;
+}
+
+NSMenu* HistoryMenuBridge::HistoryMenu() {
+ NSMenu* history_menu = [[[NSApp mainMenu] itemWithTag:IDC_HISTORY_MENU]
+ submenu];
+ return history_menu;
+}
+
+void HistoryMenuBridge::ClearMenuSection(NSMenu* menu, NSInteger tag) {
+ for (NSMenuItem* menu_item in [menu itemArray]) {
+ if ([menu_item tag] == tag && [menu_item target] == controller_.get()) {
+ // This is an item that should be removed, so find the corresponding model
+ // item.
+ HistoryItem* item = HistoryItemForMenuItem(menu_item);
+
+ // Cancel favicon requests that could hold onto stale pointers. Also
+ // remove the item from the mapping.
+ if (item) {
+ CancelFaviconRequest(item);
+ menu_item_map_.erase(menu_item);
+ delete item;
+ }
+
+ // If this menu item has a submenu, recurse.
+ if ([menu_item hasSubmenu]) {
+ ClearMenuSection([menu_item submenu], tag + 1);
+ }
+
+ // Now actually remove the item from the menu.
+ [menu removeItem:menu_item];
+ }
+ }
+}
+
+NSMenuItem* HistoryMenuBridge::AddItemToMenu(HistoryItem* item,
+ NSMenu* menu,
+ NSInteger tag,
+ NSInteger index) {
+ NSString* title = base::SysUTF16ToNSString(item->title);
+ std::string url_string = item->url.possibly_invalid_spec();
+
+ // If we don't have a title, use the URL.
+ if ([title isEqualToString:@""])
+ title = base::SysUTF8ToNSString(url_string);
+ NSString* full_title = title;
+ if ([title length] > kMaximumMenuWidthInChars) {
+ // TODO(rsesek): use app/text_elider.h once it uses string16 and can
+ // take out the middle of strings.
+ title = [NSString stringWithFormat:@"%@…%@",
+ [title substringToIndex:kMenuTrimSizeInChars],
+ [title substringFromIndex:([title length] -
+ kMenuTrimSizeInChars)]];
+ }
+ item->menu_item.reset(
+ [[NSMenuItem alloc] initWithTitle:title
+ action:nil
+ keyEquivalent:@""]);
+ [item->menu_item setTarget:controller_];
+ [item->menu_item setAction:@selector(openHistoryMenuItem:)];
+ [item->menu_item setTag:tag];
+ if (item->icon.get())
+ [item->menu_item setImage:item->icon.get()];
+ else if (!item->tabs.size())
+ [item->menu_item setImage:default_favicon_.get()];
+
+ // Add a tooltip.
+ NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", full_title,
+ url_string.c_str()];
+ [item->menu_item setToolTip:tooltip];
+
+ [menu insertItem:item->menu_item.get() atIndex:index];
+ menu_item_map_.insert(std::make_pair(item->menu_item.get(), item));
+
+ return item->menu_item.get();
+}
+
+void HistoryMenuBridge::Init() {
+ const NotificationSource& source = NotificationService::AllSources();
+ registrar_.Add(this, NotificationType::HISTORY_TYPED_URLS_MODIFIED, source);
+ registrar_.Add(this, NotificationType::HISTORY_URL_VISITED, source);
+ registrar_.Add(this, NotificationType::HISTORY_URLS_DELETED, source);
+}
+
+void HistoryMenuBridge::CreateMenu() {
+ // If we're currently running CreateMenu(), wait until it finishes.
+ if (create_in_progress_)
+ return;
+ create_in_progress_ = true;
+ need_recreate_ = false;
+
+ history_service_->QuerySegmentUsageSince(
+ &cancelable_request_consumer_,
+ base::Time::Now() - base::TimeDelta::FromDays(kMostVisitedScope),
+ kMostVisitedCount,
+ NewCallback(this, &HistoryMenuBridge::OnVisitedHistoryResults));
+}
+
+void HistoryMenuBridge::OnVisitedHistoryResults(
+ CancelableRequestProvider::Handle handle,
+ std::vector<PageUsageData*>* results) {
+ NSMenu* menu = HistoryMenu();
+ ClearMenuSection(menu, kMostVisited);
+ NSInteger top_item = [menu indexOfItemWithTag:kMostVisitedTitle] + 1;
+
+ size_t count = results->size();
+ for (size_t i = 0; i < count; ++i) {
+ PageUsageData* history_item = (*results)[i];
+
+ HistoryItem* item = new HistoryItem();
+ item->title = history_item->GetTitle();
+ item->url = history_item->GetURL();
+ if (history_item->HasFavIcon()) {
+ const SkBitmap* icon = history_item->GetFavIcon();
+ item->icon.reset([gfx::SkBitmapToNSImage(*icon) retain]);
+ } else {
+ GetFaviconForHistoryItem(item);
+ }
+ // This will add |item| to the |menu_item_map_|, which takes ownership.
+ AddItemToMenu(item, HistoryMenu(), kMostVisited, top_item + i);
+ }
+
+ // We are already invalid by the time we finished, darn.
+ if (need_recreate_)
+ CreateMenu();
+
+ create_in_progress_ = false;
+}
+
+HistoryMenuBridge::HistoryItem* HistoryMenuBridge::HistoryItemForTab(
+ const TabRestoreService::Tab& entry) {
+ if (entry.navigations.empty())
+ return NULL;
+
+ const TabNavigation& current_navigation =
+ entry.navigations.at(entry.current_navigation_index);
+ if (current_navigation.virtual_url() == GURL(chrome::kChromeUINewTabURL))
+ return NULL;
+
+ HistoryItem* item = new HistoryItem();
+ item->title = current_navigation.title();
+ item->url = current_navigation.virtual_url();
+ item->session_id = entry.id;
+
+ // Tab navigations don't come with icons, so we always have to request them.
+ GetFaviconForHistoryItem(item);
+
+ return item;
+}
+
+void HistoryMenuBridge::GetFaviconForHistoryItem(HistoryItem* item) {
+ FaviconService* service =
+ profile_->GetFaviconService(Profile::EXPLICIT_ACCESS);
+ FaviconService::Handle handle = service->GetFaviconForURL(item->url,
+ &favicon_consumer_,
+ NewCallback(this, &HistoryMenuBridge::GotFaviconData));
+ favicon_consumer_.SetClientData(service, handle, item);
+ item->icon_handle = handle;
+ item->icon_requested = true;
+}
+
+void HistoryMenuBridge::GotFaviconData(FaviconService::Handle handle,
+ bool know_favicon,
+ scoped_refptr<RefCountedMemory> data,
+ bool expired,
+ GURL url) {
+ // Since we're going to do Cocoa-y things, make sure this is the main thread.
+ DCHECK([NSThread isMainThread]);
+
+ HistoryItem* item =
+ favicon_consumer_.GetClientData(
+ profile_->GetFaviconService(Profile::EXPLICIT_ACCESS), handle);
+ DCHECK(item);
+ item->icon_requested = false;
+ item->icon_handle = NULL;
+
+ // Convert the raw data to Skia and then to a NSImage.
+ // TODO(rsesek): Is there an easier way to do this?
+ SkBitmap icon;
+ if (know_favicon && data.get() && data->size() &&
+ gfx::PNGCodec::Decode(data->front(), data->size(), &icon)) {
+ NSImage* image = gfx::SkBitmapToNSImage(icon);
+ if (image) {
+ // The conversion was successful.
+ item->icon.reset([image retain]);
+ [item->menu_item setImage:item->icon.get()];
+ }
+ }
+}
+
+void HistoryMenuBridge::CancelFaviconRequest(HistoryItem* item) {
+ DCHECK(item);
+ if (item->icon_requested) {
+ FaviconService* service =
+ profile_->GetFaviconService(Profile::EXPLICIT_ACCESS);
+ service->CancelRequest(item->icon_handle);
+ item->icon_requested = false;
+ item->icon_handle = NULL;
+ }
+}
diff --git a/chrome/browser/ui/cocoa/history_menu_bridge_unittest.mm b/chrome/browser/ui/cocoa/history_menu_bridge_unittest.mm
new file mode 100644
index 0000000..843e964
--- /dev/null
+++ b/chrome/browser/ui/cocoa/history_menu_bridge_unittest.mm
@@ -0,0 +1,386 @@
+// 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 <vector>
+
+#include "base/ref_counted_memory.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/cancelable_request.h"
+#include "chrome/browser/sessions/tab_restore_service.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/history_menu_bridge.h"
+#include "gfx/codec/png_codec.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+
+namespace {
+
+class MockTRS : public TabRestoreService {
+ public:
+ MockTRS(Profile* profile) : TabRestoreService(profile, NULL) {}
+ MOCK_CONST_METHOD0(entries, const TabRestoreService::Entries&());
+};
+
+class MockBridge : public HistoryMenuBridge {
+ public:
+ MockBridge(Profile* profile)
+ : HistoryMenuBridge(profile),
+ menu_([[NSMenu alloc] initWithTitle:@"History"]) {}
+
+ virtual NSMenu* HistoryMenu() {
+ return menu_.get();
+ }
+
+ private:
+ scoped_nsobject<NSMenu> menu_;
+};
+
+class HistoryMenuBridgeTest : public CocoaTest {
+ public:
+
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ browser_test_helper_.profile()->CreateFaviconService();
+ bridge_.reset(new MockBridge(browser_test_helper_.profile()));
+ }
+
+ // We are a friend of HistoryMenuBridge (and have access to
+ // protected methods), but none of the classes generated by TEST_F()
+ // are. Wraps common commands.
+ void ClearMenuSection(NSMenu* menu,
+ NSInteger tag) {
+ bridge_->ClearMenuSection(menu, tag);
+ }
+
+ void AddItemToBridgeMenu(HistoryMenuBridge::HistoryItem* item,
+ NSMenu* menu,
+ NSInteger tag,
+ NSInteger index) {
+ bridge_->AddItemToMenu(item, menu, tag, index);
+ }
+
+ NSMenuItem* AddItemToMenu(NSMenu* menu,
+ NSString* title,
+ SEL selector,
+ int tag) {
+ NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:title action:NULL
+ keyEquivalent:@""] autorelease];
+ [item setTag:tag];
+ if (selector) {
+ [item setAction:selector];
+ [item setTarget:bridge_->controller_.get()];
+ }
+ [menu addItem:item];
+ return item;
+ }
+
+ HistoryMenuBridge::HistoryItem* CreateItem(const string16& title) {
+ HistoryMenuBridge::HistoryItem* item =
+ new HistoryMenuBridge::HistoryItem();
+ item->title = title;
+ item->url = GURL(title);
+ return item;
+ }
+
+ MockTRS::Tab CreateSessionTab(const GURL& url, const string16& title) {
+ MockTRS::Tab tab;
+ tab.current_navigation_index = 0;
+ tab.navigations.push_back(
+ TabNavigation(0, url, GURL(), title, std::string(),
+ PageTransition::LINK));
+ return tab;
+ }
+
+ void GetFaviconForHistoryItem(HistoryMenuBridge::HistoryItem* item) {
+ bridge_->GetFaviconForHistoryItem(item);
+ }
+
+ void GotFaviconData(FaviconService::Handle handle,
+ bool know_favicon,
+ scoped_refptr<RefCountedBytes> data,
+ bool expired,
+ GURL url) {
+ bridge_->GotFaviconData(handle, know_favicon, data, expired, url);
+ }
+
+ CancelableRequestConsumerTSimple<HistoryMenuBridge::HistoryItem*>&
+ favicon_consumer() {
+ return bridge_->favicon_consumer_;
+ }
+
+ BrowserTestHelper browser_test_helper_;
+ scoped_ptr<MockBridge> bridge_;
+};
+
+// Edge case test for clearing until the end of a menu.
+TEST_F(HistoryMenuBridgeTest, ClearHistoryMenuUntilEnd) {
+ NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"history foo"] autorelease];
+ AddItemToMenu(menu, @"HEADER", NULL, HistoryMenuBridge::kMostVisitedTitle);
+
+ NSInteger tag = HistoryMenuBridge::kMostVisited;
+ AddItemToMenu(menu, @"alpha", @selector(openHistoryMenuItem:), tag);
+ AddItemToMenu(menu, @"bravo", @selector(openHistoryMenuItem:), tag);
+ AddItemToMenu(menu, @"charlie", @selector(openHistoryMenuItem:), tag);
+ AddItemToMenu(menu, @"delta", @selector(openHistoryMenuItem:), tag);
+
+ ClearMenuSection(menu, HistoryMenuBridge::kMostVisited);
+
+ EXPECT_EQ(1, [menu numberOfItems]);
+ EXPECT_NSEQ(@"HEADER",
+ [[menu itemWithTag:HistoryMenuBridge::kMostVisitedTitle] title]);
+}
+
+// Skip menu items that are not hooked up to |-openHistoryMenuItem:|.
+TEST_F(HistoryMenuBridgeTest, ClearHistoryMenuSkipping) {
+ NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"history foo"] autorelease];
+ AddItemToMenu(menu, @"HEADER", NULL, HistoryMenuBridge::kMostVisitedTitle);
+
+ NSInteger tag = HistoryMenuBridge::kMostVisited;
+ AddItemToMenu(menu, @"alpha", @selector(openHistoryMenuItem:), tag);
+ AddItemToMenu(menu, @"bravo", @selector(openHistoryMenuItem:), tag);
+ AddItemToMenu(menu, @"TITLE", NULL, HistoryMenuBridge::kRecentlyClosedTitle);
+ AddItemToMenu(menu, @"charlie", @selector(openHistoryMenuItem:), tag);
+
+ ClearMenuSection(menu, tag);
+
+ EXPECT_EQ(2, [menu numberOfItems]);
+ EXPECT_NSEQ(@"HEADER",
+ [[menu itemWithTag:HistoryMenuBridge::kMostVisitedTitle] title]);
+ EXPECT_NSEQ(@"TITLE",
+ [[menu itemAtIndex:1] title]);
+}
+
+// Edge case test for clearing an empty menu.
+TEST_F(HistoryMenuBridgeTest, ClearHistoryMenuEmpty) {
+ NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"history foo"] autorelease];
+ AddItemToMenu(menu, @"HEADER", NULL, HistoryMenuBridge::kMostVisited);
+
+ ClearMenuSection(menu, HistoryMenuBridge::kMostVisited);
+
+ EXPECT_EQ(1, [menu numberOfItems]);
+ EXPECT_NSEQ(@"HEADER",
+ [[menu itemWithTag:HistoryMenuBridge::kMostVisited] title]);
+}
+
+// Test that AddItemToMenu() properly adds HistoryItem objects as menus.
+TEST_F(HistoryMenuBridgeTest, AddItemToMenu) {
+ NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"history foo"] autorelease];
+
+ const string16 short_url = ASCIIToUTF16("http://foo/");
+ const string16 long_url = ASCIIToUTF16("http://super-duper-long-url--."
+ "that.cannot.possibly.fit.even-in-80-columns"
+ "or.be.reasonably-displayed-in-a-menu"
+ "without.looking-ridiculous.com/"); // 140 chars total
+
+ // HistoryItems are owned by the HistoryMenuBridge when AddItemToBridgeMenu()
+ // is called, which places them into the |menu_item_map_|, which owns them.
+ HistoryMenuBridge::HistoryItem* item1 = CreateItem(short_url);
+ AddItemToBridgeMenu(item1, menu, 100, 0);
+
+ HistoryMenuBridge::HistoryItem* item2 = CreateItem(long_url);
+ AddItemToBridgeMenu(item2, menu, 101, 1);
+
+ EXPECT_EQ(2, [menu numberOfItems]);
+
+ EXPECT_EQ(@selector(openHistoryMenuItem:), [[menu itemAtIndex:0] action]);
+ EXPECT_EQ(@selector(openHistoryMenuItem:), [[menu itemAtIndex:1] action]);
+
+ EXPECT_EQ(100, [[menu itemAtIndex:0] tag]);
+ EXPECT_EQ(101, [[menu itemAtIndex:1] tag]);
+
+ // Make sure a short title looks fine
+ NSString* s = [[menu itemAtIndex:0] title];
+ EXPECT_EQ(base::SysNSStringToUTF16(s), short_url);
+
+ // Make sure a super-long title gets trimmed
+ s = [[menu itemAtIndex:0] title];
+ EXPECT_TRUE([s length] < long_url.length());
+
+ // Confirm tooltips and confirm they are not trimmed (like the item
+ // name might be). Add tolerance for URL fixer-upping;
+ // e.g. http://foo becomes http://foo/)
+ EXPECT_GE([[[menu itemAtIndex:0] toolTip] length], (2*short_url.length()-5));
+ EXPECT_GE([[[menu itemAtIndex:1] toolTip] length], (2*long_url.length()-5));
+}
+
+// Test that the menu is created for a set of simple tabs.
+TEST_F(HistoryMenuBridgeTest, RecentlyClosedTabs) {
+ scoped_refptr<MockTRS> trs(new MockTRS(browser_test_helper_.profile()));
+ MockTRS::Entries entries;
+
+ MockTRS::Tab tab1 = CreateSessionTab(GURL("http://google.com"),
+ ASCIIToUTF16("Google"));
+ tab1.id = 24;
+ entries.push_back(&tab1);
+
+ MockTRS::Tab tab2 = CreateSessionTab(GURL("http://apple.com"),
+ ASCIIToUTF16("Apple"));
+ tab2.id = 42;
+ entries.push_back(&tab2);
+
+ using ::testing::ReturnRef;
+ EXPECT_CALL(*trs.get(), entries()).WillOnce(ReturnRef(entries));
+
+ bridge_->TabRestoreServiceChanged(trs.get());
+
+ NSMenu* menu = bridge_->HistoryMenu();
+ ASSERT_EQ(2U, [[menu itemArray] count]);
+
+ NSMenuItem* item1 = [menu itemAtIndex:0];
+ MockBridge::HistoryItem* hist1 = bridge_->HistoryItemForMenuItem(item1);
+ EXPECT_TRUE(hist1);
+ EXPECT_EQ(24, hist1->session_id);
+ EXPECT_NSEQ(@"Google", [item1 title]);
+
+ NSMenuItem* item2 = [menu itemAtIndex:1];
+ MockBridge::HistoryItem* hist2 = bridge_->HistoryItemForMenuItem(item2);
+ EXPECT_TRUE(hist2);
+ EXPECT_EQ(42, hist2->session_id);
+ EXPECT_NSEQ(@"Apple", [item2 title]);
+}
+
+// Test that the menu is created for a mix of windows and tabs.
+TEST_F(HistoryMenuBridgeTest, RecentlyClosedTabsAndWindows) {
+ scoped_refptr<MockTRS> trs(new MockTRS(browser_test_helper_.profile()));
+ MockTRS::Entries entries;
+
+ MockTRS::Tab tab1 = CreateSessionTab(GURL("http://google.com"),
+ ASCIIToUTF16("Google"));
+ tab1.id = 24;
+ entries.push_back(&tab1);
+
+ MockTRS::Window win1;
+ win1.id = 30;
+ win1.tabs.push_back(
+ CreateSessionTab(GURL("http://foo.com"), ASCIIToUTF16("foo")));
+ win1.tabs[0].id = 31;
+ win1.tabs.push_back(
+ CreateSessionTab(GURL("http://bar.com"), ASCIIToUTF16("bar")));
+ win1.tabs[1].id = 32;
+ entries.push_back(&win1);
+
+ MockTRS::Tab tab2 = CreateSessionTab(GURL("http://apple.com"),
+ ASCIIToUTF16("Apple"));
+ tab2.id = 42;
+ entries.push_back(&tab2);
+
+ MockTRS::Window win2;
+ win2.id = 50;
+ win2.tabs.push_back(
+ CreateSessionTab(GURL("http://magic.com"), ASCIIToUTF16("magic")));
+ win2.tabs[0].id = 51;
+ win2.tabs.push_back(
+ CreateSessionTab(GURL("http://goats.com"), ASCIIToUTF16("goats")));
+ win2.tabs[1].id = 52;
+ win2.tabs.push_back(
+ CreateSessionTab(GURL("http://teleporter.com"),
+ ASCIIToUTF16("teleporter")));
+ win2.tabs[1].id = 53;
+ entries.push_back(&win2);
+
+ using ::testing::ReturnRef;
+ EXPECT_CALL(*trs.get(), entries()).WillOnce(ReturnRef(entries));
+
+ bridge_->TabRestoreServiceChanged(trs.get());
+
+ NSMenu* menu = bridge_->HistoryMenu();
+ ASSERT_EQ(4U, [[menu itemArray] count]);
+
+ NSMenuItem* item1 = [menu itemAtIndex:0];
+ MockBridge::HistoryItem* hist1 = bridge_->HistoryItemForMenuItem(item1);
+ EXPECT_TRUE(hist1);
+ EXPECT_EQ(24, hist1->session_id);
+ EXPECT_NSEQ(@"Google", [item1 title]);
+
+ NSMenuItem* item2 = [menu itemAtIndex:1];
+ MockBridge::HistoryItem* hist2 = bridge_->HistoryItemForMenuItem(item2);
+ EXPECT_TRUE(hist2);
+ EXPECT_EQ(30, hist2->session_id);
+ EXPECT_EQ(2U, hist2->tabs.size());
+ // Do not test menu item title because it is localized.
+ NSMenu* submenu1 = [item2 submenu];
+ EXPECT_EQ(4U, [[submenu1 itemArray] count]);
+ // Do not test Restore All Tabs because it is localiced.
+ EXPECT_TRUE([[submenu1 itemAtIndex:1] isSeparatorItem]);
+ EXPECT_NSEQ(@"foo", [[submenu1 itemAtIndex:2] title]);
+ EXPECT_NSEQ(@"bar", [[submenu1 itemAtIndex:3] title]);
+
+ NSMenuItem* item3 = [menu itemAtIndex:2];
+ MockBridge::HistoryItem* hist3 = bridge_->HistoryItemForMenuItem(item3);
+ EXPECT_TRUE(hist3);
+ EXPECT_EQ(42, hist3->session_id);
+ EXPECT_NSEQ(@"Apple", [item3 title]);
+
+ NSMenuItem* item4 = [menu itemAtIndex:3];
+ MockBridge::HistoryItem* hist4 = bridge_->HistoryItemForMenuItem(item4);
+ EXPECT_TRUE(hist4);
+ EXPECT_EQ(50, hist4->session_id);
+ EXPECT_EQ(3U, hist4->tabs.size());
+ // Do not test menu item title because it is localized.
+ NSMenu* submenu2 = [item4 submenu];
+ EXPECT_EQ(5U, [[submenu2 itemArray] count]);
+ // Do not test Restore All Tabs because it is localiced.
+ EXPECT_TRUE([[submenu2 itemAtIndex:1] isSeparatorItem]);
+ EXPECT_NSEQ(@"magic", [[submenu2 itemAtIndex:2] title]);
+ EXPECT_NSEQ(@"goats", [[submenu2 itemAtIndex:3] title]);
+ EXPECT_NSEQ(@"teleporter", [[submenu2 itemAtIndex:4] title]);
+}
+
+// Tests that we properly request an icon from the FaviconService.
+TEST_F(HistoryMenuBridgeTest, GetFaviconForHistoryItem) {
+ // Create a fake item.
+ HistoryMenuBridge::HistoryItem item;
+ item.title = ASCIIToUTF16("Title");
+ item.url = GURL("http://google.com");
+
+ // Request the icon.
+ GetFaviconForHistoryItem(&item);
+
+ // Make sure that there is ClientData for the request.
+ std::vector<HistoryMenuBridge::HistoryItem*> data;
+ favicon_consumer().GetAllClientData(&data);
+ ASSERT_EQ(data.size(), 1U);
+ EXPECT_EQ(&item, data[0]);
+
+ // Make sure the item was modified properly.
+ EXPECT_TRUE(item.icon_requested);
+ EXPECT_GT(item.icon_handle, 0);
+}
+
+TEST_F(HistoryMenuBridgeTest, GotFaviconData) {
+ // Create a dummy bitmap.
+ SkBitmap bitmap;
+ bitmap.setConfig(SkBitmap::kARGB_8888_Config, 25, 25);
+ bitmap.allocPixels();
+ bitmap.eraseRGB(255, 0, 0);
+
+ // Convert it to raw PNG bytes. We totally ignore color order here because
+ // we just want to test the roundtrip through the Bridge, not that we can
+ // make icons look pretty.
+ std::vector<unsigned char> raw;
+ gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, true, &raw);
+ scoped_refptr<RefCountedBytes> bytes(new RefCountedBytes(raw));
+
+ // Set up the HistoryItem.
+ HistoryMenuBridge::HistoryItem item;
+ item.menu_item.reset([[NSMenuItem alloc] init]);
+ GetFaviconForHistoryItem(&item);
+
+ // Pretend to be called back.
+ GotFaviconData(item.icon_handle, true, bytes, false, GURL());
+
+ // Make sure the callback works.
+ EXPECT_FALSE(item.icon_requested);
+ EXPECT_TRUE(item.icon.get());
+ EXPECT_TRUE([item.menu_item image]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/history_menu_cocoa_controller.h b/chrome/browser/ui/cocoa/history_menu_cocoa_controller.h
new file mode 100644
index 0000000..d91409e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/history_menu_cocoa_controller.h
@@ -0,0 +1,32 @@
+// 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.
+
+// Controller (MVC) for the history menu. All history menu item commands get
+// directed here. This class only responds to menu events, but the actual
+// creation and maintenance of the menu happens in the Bridge.
+
+#ifndef CHROME_BROWSER_UI_COCOA_HISTORY_MENU_COCOA_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_HISTORY_MENU_COCOA_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+#import "chrome/browser/ui/cocoa/history_menu_bridge.h"
+
+@interface HistoryMenuCocoaController : NSObject {
+ @private
+ HistoryMenuBridge* bridge_; // weak; owns us
+}
+
+- (id)initWithBridge:(HistoryMenuBridge*)bridge;
+
+// Called by any history menu item.
+- (IBAction)openHistoryMenuItem:(id)sender;
+
+@end // HistoryMenuCocoaController
+
+@interface HistoryMenuCocoaController (ExposedForUnitTests)
+- (void)openURLForItem:(const HistoryMenuBridge::HistoryItem*)node;
+@end // HistoryMenuCocoaController (ExposedForUnitTests)
+
+#endif // CHROME_BROWSER_UI_COCOA_HISTORY_MENU_COCOA_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/history_menu_cocoa_controller.mm b/chrome/browser/ui/cocoa/history_menu_cocoa_controller.mm
new file mode 100644
index 0000000..79aef69
--- /dev/null
+++ b/chrome/browser/ui/cocoa/history_menu_cocoa_controller.mm
@@ -0,0 +1,58 @@
+// 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/ui/cocoa/history_menu_cocoa_controller.h"
+
+#include "base/scoped_vector.h"
+#include "chrome/app/chrome_command_ids.h" // IDC_HISTORY_MENU
+#import "chrome/browser/app_controller_mac.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/history/history.h"
+#include "chrome/browser/history/history_types.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/cocoa/event_utils.h"
+#include "webkit/glue/window_open_disposition.h"
+
+@implementation HistoryMenuCocoaController
+
+- (id)initWithBridge:(HistoryMenuBridge*)bridge {
+ if ((self = [super init])) {
+ bridge_ = bridge;
+ DCHECK(bridge_);
+ }
+ return self;
+}
+
+- (BOOL)validateMenuItem:(NSMenuItem*)menuItem {
+ AppController* controller = [NSApp delegate];
+ return [controller keyWindowIsNotModal];
+}
+
+// Open the URL of the given history item in the current tab.
+- (void)openURLForItem:(const HistoryMenuBridge::HistoryItem*)node {
+ Browser* browser = Browser::GetOrCreateTabbedBrowser(bridge_->profile());
+ WindowOpenDisposition disposition =
+ event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
+
+ // If this item can be restored using TabRestoreService, do so. Otherwise,
+ // just load the URL.
+ TabRestoreService* service = bridge_->profile()->GetTabRestoreService();
+ if (node->session_id && service) {
+ service->RestoreEntryById(browser, node->session_id, false);
+ } else {
+ DCHECK(node->url.is_valid());
+ browser->OpenURL(node->url, GURL(), disposition,
+ PageTransition::AUTO_BOOKMARK);
+ }
+}
+
+- (IBAction)openHistoryMenuItem:(id)sender {
+ const HistoryMenuBridge::HistoryItem* item =
+ bridge_->HistoryItemForMenuItem(sender);
+ [self openURLForItem:item];
+}
+
+@end // HistoryMenuCocoaController
diff --git a/chrome/browser/ui/cocoa/history_menu_cocoa_controller_unittest.mm b/chrome/browser/ui/cocoa/history_menu_cocoa_controller_unittest.mm
new file mode 100644
index 0000000..26f7388
--- /dev/null
+++ b/chrome/browser/ui/cocoa/history_menu_cocoa_controller_unittest.mm
@@ -0,0 +1,91 @@
+// 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"
+#include "base/scoped_ptr.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/history_menu_bridge.h"
+#include "chrome/browser/ui/cocoa/history_menu_cocoa_controller.h"
+#include "chrome/browser/ui/browser.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+@interface FakeHistoryMenuController : HistoryMenuCocoaController {
+ @public
+ BOOL opened_[2];
+}
+@end
+
+@implementation FakeHistoryMenuController
+
+- (id)initTest {
+ if ((self = [super init])) {
+ opened_[0] = NO;
+ opened_[1] = NO;
+ }
+ return self;
+}
+
+- (void)openURLForItem:(HistoryMenuBridge::HistoryItem*)item {
+ opened_[item->session_id] = YES;
+}
+
+@end // FakeHistoryMenuController
+
+class HistoryMenuCocoaControllerTest : public CocoaTest {
+ public:
+
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ bridge_.reset(new HistoryMenuBridge(browser_test_helper_.profile()));
+ bridge_->controller_.reset(
+ [[FakeHistoryMenuController alloc] initWithBridge:bridge_.get()]);
+ [controller() initTest];
+ }
+
+ void CreateItems(NSMenu* menu) {
+ HistoryMenuBridge::HistoryItem* item = new HistoryMenuBridge::HistoryItem();
+ item->url = GURL("http://google.com");
+ item->session_id = 0;
+ bridge_->AddItemToMenu(item, menu, HistoryMenuBridge::kMostVisited, 0);
+
+ item = new HistoryMenuBridge::HistoryItem();
+ item->url = GURL("http://apple.com");
+ item->session_id = 1;
+ bridge_->AddItemToMenu(item, menu, HistoryMenuBridge::kMostVisited, 1);
+ }
+
+ std::map<NSMenuItem*, HistoryMenuBridge::HistoryItem*>& menu_item_map() {
+ return bridge_->menu_item_map_;
+ }
+
+ FakeHistoryMenuController* controller() {
+ return static_cast<FakeHistoryMenuController*>(bridge_->controller_.get());
+ }
+
+ private:
+ BrowserTestHelper browser_test_helper_;
+ scoped_ptr<HistoryMenuBridge> bridge_;
+};
+
+TEST_F(HistoryMenuCocoaControllerTest, OpenURLForItem) {
+
+ scoped_nsobject<NSMenu> menu([[NSMenu alloc] initWithTitle:@"History"]);
+ CreateItems(menu.get());
+
+ std::map<NSMenuItem*, HistoryMenuBridge::HistoryItem*>& items =
+ menu_item_map();
+ std::map<NSMenuItem*, HistoryMenuBridge::HistoryItem*>::iterator it =
+ items.begin();
+
+ for ( ; it != items.end(); ++it) {
+ HistoryMenuBridge::HistoryItem* item = it->second;
+ EXPECT_FALSE(controller()->opened_[item->session_id]);
+ [controller() openHistoryMenuItem:it->first];
+ EXPECT_TRUE(controller()->opened_[item->session_id]);
+ }
+}
diff --git a/chrome/browser/ui/cocoa/hover_button.h b/chrome/browser/ui/cocoa/hover_button.h
new file mode 100644
index 0000000..e411434
--- /dev/null
+++ b/chrome/browser/ui/cocoa/hover_button.h
@@ -0,0 +1,35 @@
+// 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"
+
+// A button that changes when you hover over it and click it.
+@interface HoverButton : NSButton {
+ @protected
+ // Enumeration of the hover states that the close button can be in at any one
+ // time. The button cannot be in more than one hover state at a time.
+ enum HoverState {
+ kHoverStateNone = 0,
+ kHoverStateMouseOver = 1,
+ kHoverStateMouseDown = 2
+ };
+
+ HoverState hoverState_;
+
+ @private
+ // Tracking area for button mouseover states.
+ scoped_nsobject<NSTrackingArea> trackingArea_;
+}
+
+// Enables or disables the |NSTrackingRect|s for the button.
+- (void)setTrackingEnabled:(BOOL)enabled;
+
+// Checks to see whether the mouse is in the button's bounds and update
+// the image in case it gets out of sync. This occurs to the close button
+// when you close a tab so the tab to the left of it takes its place, and
+// drag the button without moving the mouse before you press the button down.
+- (void)checkImageState;
+@end
diff --git a/chrome/browser/ui/cocoa/hover_button.mm b/chrome/browser/ui/cocoa/hover_button.mm
new file mode 100644
index 0000000..9d7a412
--- /dev/null
+++ b/chrome/browser/ui/cocoa/hover_button.mm
@@ -0,0 +1,97 @@
+// 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/ui/cocoa/hover_button.h"
+
+@implementation HoverButton
+
+- (id)initWithFrame:(NSRect)frameRect {
+ if ((self = [super initWithFrame:frameRect])) {
+ [self setTrackingEnabled:YES];
+ hoverState_ = kHoverStateNone;
+ [self updateTrackingAreas];
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ [self setTrackingEnabled:YES];
+ hoverState_ = kHoverStateNone;
+ [self updateTrackingAreas];
+}
+
+- (void)dealloc {
+ [self setTrackingEnabled:NO];
+ [super dealloc];
+}
+
+- (void)mouseEntered:(NSEvent*)theEvent {
+ hoverState_ = kHoverStateMouseOver;
+ [self setNeedsDisplay:YES];
+}
+
+- (void)mouseExited:(NSEvent*)theEvent {
+ hoverState_ = kHoverStateNone;
+ [self setNeedsDisplay:YES];
+}
+
+- (void)mouseDown:(NSEvent*)theEvent {
+ hoverState_ = kHoverStateMouseDown;
+ [self setNeedsDisplay:YES];
+ // The hover button needs to hold onto itself here for a bit. Otherwise,
+ // it can be freed while |super mouseDown:| is in it's loop, and the
+ // |checkImageState| call will crash.
+ // http://crbug.com/28220
+ scoped_nsobject<HoverButton> myself([self retain]);
+
+ [super mouseDown:theEvent];
+ // We need to check the image state after the mouseDown event loop finishes.
+ // It's possible that we won't get a mouseExited event if the button was
+ // moved under the mouse during tab resize, instead of the mouse moving over
+ // the button.
+ // http://crbug.com/31279
+ [self checkImageState];
+}
+
+- (void)setTrackingEnabled:(BOOL)enabled {
+ if (enabled) {
+ trackingArea_.reset(
+ [[NSTrackingArea alloc] initWithRect:[self bounds]
+ options:NSTrackingMouseEnteredAndExited |
+ NSTrackingActiveAlways
+ owner:self
+ userInfo:nil]);
+ [self addTrackingArea:trackingArea_.get()];
+
+ // If you have a separate window that overlaps the close button, and you
+ // move the mouse directly over the close button without entering another
+ // part of the tab strip, we don't get any mouseEntered event since the
+ // tracking area was disabled when we entered.
+ [self checkImageState];
+ } else {
+ if (trackingArea_.get()) {
+ [self removeTrackingArea:trackingArea_.get()];
+ trackingArea_.reset(nil);
+ }
+ }
+}
+
+- (void)updateTrackingAreas {
+ [super updateTrackingAreas];
+ [self checkImageState];
+}
+
+- (void)checkImageState {
+ if (!trackingArea_.get())
+ return;
+
+ // Update the button's state if the button has moved.
+ NSPoint mouseLoc = [[self window] mouseLocationOutsideOfEventStream];
+ mouseLoc = [self convertPoint:mouseLoc fromView:nil];
+ hoverState_ = NSPointInRect(mouseLoc, [self bounds]) ?
+ kHoverStateMouseOver : kHoverStateNone;
+ [self setNeedsDisplay:YES];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/hover_close_button.h b/chrome/browser/ui/cocoa/hover_close_button.h
new file mode 100644
index 0000000..372582c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/hover_close_button.h
@@ -0,0 +1,26 @@
+// 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"
+#include "chrome/browser/ui/cocoa/hover_button.h"
+
+// The standard close button for our Mac UI which is the "x" that changes to a
+// dark circle with the "x" when you hover over it. At this time it is used by
+// the popup blocker, download bar, info bar and tabs.
+@interface HoverCloseButton : HoverButton {
+ @private
+ // Bezier path for drawing the 'x' within the button.
+ scoped_nsobject<NSBezierPath> xPath_;
+
+ // Bezier path for drawing the hover state circle behind the 'x'.
+ scoped_nsobject<NSBezierPath> circlePath_;
+}
+
+// Sets up the button's tracking areas and accessibility info when instantiated
+// via initWithFrame or awakeFromNib.
+- (void)commonInit;
+
+@end
diff --git a/chrome/browser/ui/cocoa/hover_close_button.mm b/chrome/browser/ui/cocoa/hover_close_button.mm
new file mode 100644
index 0000000..f8e29e2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/hover_close_button.mm
@@ -0,0 +1,108 @@
+// 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/ui/cocoa/hover_close_button.h"
+
+#include "app/l10n_util.h"
+#include "base/scoped_nsobject.h"
+#include "grit/generated_resources.h"
+#import "third_party/molokocacao/NSBezierPath+MCAdditions.h"
+
+namespace {
+// Convenience function to return the middle point of the given |rect|.
+static NSPoint MidRect(NSRect rect) {
+ return NSMakePoint(NSMidX(rect), NSMidY(rect));
+}
+
+const CGFloat kCircleRadiusPercentage = 0.415;
+const CGFloat kCircleHoverWhite = 0.565;
+const CGFloat kCircleClickWhite = 0.396;
+const CGFloat kXShadowAlpha = 0.75;
+const CGFloat kXShadowCircleAlpha = 0.1;
+} // namespace
+
+@interface HoverCloseButton(Private)
+- (void)setUpDrawingPaths;
+@end
+
+@implementation HoverCloseButton
+
+- (id)initWithFrame:(NSRect)frameRect {
+ if ((self = [super initWithFrame:frameRect])) {
+ [self commonInit];
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ [super awakeFromNib];
+ [self commonInit];
+}
+
+- (void)drawRect:(NSRect)rect {
+ if (!circlePath_.get() || !xPath_.get())
+ [self setUpDrawingPaths];
+
+ // If the user is hovering over the button, a light/dark gray circle is drawn
+ // behind the 'x'.
+ if (hoverState_ != kHoverStateNone) {
+ // Adjust the darkness of the circle depending on whether it is being
+ // clicked.
+ CGFloat white = (hoverState_ == kHoverStateMouseOver) ?
+ kCircleHoverWhite : kCircleClickWhite;
+ [[NSColor colorWithCalibratedWhite:white alpha:1.0] set];
+ [circlePath_ fill];
+ }
+
+ [[NSColor whiteColor] set];
+ [xPath_ fill];
+
+ // Give the 'x' an inner shadow for depth. If the button is in a hover state
+ // (circle behind it), then adjust the shadow accordingly (not as harsh).
+ NSShadow* shadow = [[[NSShadow alloc] init] autorelease];
+ CGFloat alpha = (hoverState_ != kHoverStateNone) ?
+ kXShadowCircleAlpha : kXShadowAlpha;
+ [shadow setShadowColor:[NSColor colorWithCalibratedWhite:0.15
+ alpha:alpha]];
+ [shadow setShadowOffset:NSMakeSize(0.0, 0.0)];
+ [shadow setShadowBlurRadius:2.5];
+ [xPath_ fillWithInnerShadow:shadow];
+}
+
+- (void)commonInit {
+ // Set accessibility description.
+ NSString* description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_CLOSE);
+ [[self cell]
+ accessibilitySetOverrideValue:description
+ forAttribute:NSAccessibilityDescriptionAttribute];
+}
+
+- (void)setUpDrawingPaths {
+ NSPoint viewCenter = MidRect([self bounds]);
+
+ circlePath_.reset([[NSBezierPath bezierPath] retain]);
+ [circlePath_ moveToPoint:viewCenter];
+ CGFloat radius = kCircleRadiusPercentage * NSWidth([self bounds]);
+ [circlePath_ appendBezierPathWithArcWithCenter:viewCenter
+ radius:radius
+ startAngle:0.0
+ endAngle:365.0];
+
+ // Construct an 'x' by drawing two intersecting rectangles in the shape of a
+ // cross and then rotating the path by 45 degrees.
+ xPath_.reset([[NSBezierPath bezierPath] retain]);
+ [xPath_ appendBezierPathWithRect:NSMakeRect(3.5, 7.0, 9.0, 2.0)];
+ [xPath_ appendBezierPathWithRect:NSMakeRect(7.0, 3.5, 2.0, 9.0)];
+
+ NSPoint pathCenter = MidRect([xPath_ bounds]);
+
+ NSAffineTransform* transform = [NSAffineTransform transform];
+ [transform translateXBy:viewCenter.x yBy:viewCenter.y];
+ [transform rotateByDegrees:45.0];
+ [transform translateXBy:-pathCenter.x yBy:-pathCenter.y];
+
+ [xPath_ transformUsingAffineTransform:transform];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/hover_image_button.h b/chrome/browser/ui/cocoa/hover_image_button.h
new file mode 100644
index 0000000..76a702d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/hover_image_button.h
@@ -0,0 +1,40 @@
+// 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"
+#include "chrome/browser/ui/cocoa/hover_button.h"
+
+// A button that changes images when you hover over it and click it.
+@interface HoverImageButton : HoverButton {
+ @private
+ float defaultOpacity_;
+ float hoverOpacity_;
+ float pressedOpacity_;
+
+ scoped_nsobject<NSImage> defaultImage_;
+ scoped_nsobject<NSImage> hoverImage_;
+ scoped_nsobject<NSImage> pressedImage_;
+}
+
+// Sets the default image.
+- (void)setDefaultImage:(NSImage*)image;
+
+// Sets the hover image.
+- (void)setHoverImage:(NSImage*)image;
+
+// Sets the pressed image.
+- (void)setPressedImage:(NSImage*)image;
+
+// Sets the default opacity.
+- (void)setDefaultOpacity:(float)opacity;
+
+// Sets the opacity on hover.
+- (void)setHoverOpacity:(float)opacity;
+
+// Sets the opacity when pressed.
+- (void)setPressedOpacity:(float)opacity;
+
+@end
diff --git a/chrome/browser/ui/cocoa/hover_image_button.mm b/chrome/browser/ui/cocoa/hover_image_button.mm
new file mode 100644
index 0000000..c5bdbf4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/hover_image_button.mm
@@ -0,0 +1,52 @@
+// 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/ui/cocoa/hover_image_button.h"
+
+#include "app/l10n_util.h"
+#include "base/scoped_nsobject.h"
+#include "grit/generated_resources.h"
+
+@implementation HoverImageButton
+
+- (void)drawRect:(NSRect)rect {
+ if (hoverState_ == kHoverStateMouseDown && pressedImage_) {
+ [super setImage:pressedImage_.get()];
+ [super setAlphaValue:pressedOpacity_];
+ } else if (hoverState_ == kHoverStateMouseOver && hoverImage_) {
+ [super setImage:hoverImage_.get()];
+ [super setAlphaValue:hoverOpacity_];
+ } else {
+ [super setImage:defaultImage_.get()];
+ [super setAlphaValue:defaultOpacity_];
+ }
+
+ [super drawRect:rect];
+}
+
+- (void)setDefaultImage:(NSImage*)image {
+ defaultImage_.reset([image retain]);
+}
+
+- (void)setDefaultOpacity:(float)opacity {
+ defaultOpacity_ = opacity;
+}
+
+- (void)setHoverImage:(NSImage*)image {
+ hoverImage_.reset([image retain]);
+}
+
+- (void)setHoverOpacity:(float)opacity {
+ hoverOpacity_ = opacity;
+}
+
+- (void)setPressedImage:(NSImage*)image {
+ pressedImage_.reset([image retain]);
+}
+
+- (void)setPressedOpacity:(float)opacity {
+ pressedOpacity_ = opacity;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/hover_image_button_unittest.mm b/chrome/browser/ui/cocoa/hover_image_button_unittest.mm
new file mode 100644
index 0000000..d2db766
--- /dev/null
+++ b/chrome/browser/ui/cocoa/hover_image_button_unittest.mm
@@ -0,0 +1,67 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "app/resource_bundle.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/hover_image_button.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+#include "grit/theme_resources.h"
+
+namespace {
+
+class HoverImageButtonTest : public CocoaTest {
+ public:
+ HoverImageButtonTest() {
+ NSRect content_frame = [[test_window() contentView] frame];
+ scoped_nsobject<HoverImageButton> button(
+ [[HoverImageButton alloc] initWithFrame:content_frame]);
+ button_ = button.get();
+ [[test_window() contentView] addSubview:button_];
+ }
+
+ virtual void SetUp() {
+ CocoaTest::BootstrapCocoa();
+ }
+
+ HoverImageButton* button_;
+};
+
+// Test mouse events.
+TEST_F(HoverImageButtonTest, ImageSwap) {
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ NSImage* image = rb.GetNativeImageNamed(IDR_HOME);
+ NSImage* hover = rb.GetNativeImageNamed(IDR_BACK);
+ [button_ setDefaultImage:image];
+ [button_ setHoverImage:hover];
+
+ [button_ mouseEntered:nil];
+ [button_ drawRect:[button_ frame]];
+ EXPECT_EQ([button_ image], hover);
+ [button_ mouseExited:nil];
+ [button_ drawRect:[button_ frame]];
+ EXPECT_EQ([button_ image], image);
+}
+
+// Test mouse events.
+TEST_F(HoverImageButtonTest, Opacity) {
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ NSImage* image = rb.GetNativeImageNamed(IDR_HOME);
+ [button_ setDefaultImage:image];
+ [button_ setDefaultOpacity:0.5];
+ [button_ setHoverImage:image];
+ [button_ setHoverOpacity:1.0];
+
+ [button_ mouseEntered:nil];
+ [button_ drawRect:[button_ frame]];
+ EXPECT_EQ([button_ alphaValue], 1.0);
+ [button_ mouseExited:nil];
+ [button_ drawRect:[button_ frame]];
+ EXPECT_EQ([button_ alphaValue], 0.5);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/html_dialog_window_controller.h b/chrome/browser/ui/cocoa/html_dialog_window_controller.h
new file mode 100644
index 0000000..3fa41a3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/html_dialog_window_controller.h
@@ -0,0 +1,55 @@
+// 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_UI_COCOA_HTML_DIALOG_WINDOW_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_HTML_DIALOG_WINDOW_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/basictypes.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/dom_ui/html_dialog_ui.h"
+
+class HtmlDialogWindowDelegateBridge;
+class Profile;
+class TabContents;
+
+// This controller manages a dialog box with properties and HTML content taken
+// from a HTMLDialogUIDelegate object.
+@interface HtmlDialogWindowController : NSWindowController {
+ @private
+ // Order here is important, as tab_contents_ may send messages to
+ // delegate_ when it gets destroyed.
+ scoped_ptr<HtmlDialogWindowDelegateBridge> delegate_;
+ scoped_ptr<TabContents> tabContents_;
+}
+
+// Creates and shows an HtmlDialogWindowController with the given
+// delegate and profile. The window is automatically destroyed when
+// it is closed. Returns the created window.
+//
+// Make sure to use the returned window only when you know it is safe
+// to do so, i.e. before OnDialogClosed() is called on the delegate.
++ (NSWindow*)showHtmlDialog:(HtmlDialogUIDelegate*)delegate
+ profile:(Profile*)profile;
+
+@end
+
+@interface HtmlDialogWindowController (TestingAPI)
+
+// This is the designated initializer. However, this is exposed only
+// for testing; use showHtmlDialog instead.
+- (id)initWithDelegate:(HtmlDialogUIDelegate*)delegate
+ profile:(Profile*)profile;
+
+// Loads the HTML content from the delegate; this is not a lightweight
+// process which is why it is not part of the constructor. Must be
+// called before showWindow.
+- (void)loadDialogContents;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_HTML_DIALOG_WINDOW_CONTROLLER_H_
+
diff --git a/chrome/browser/ui/cocoa/html_dialog_window_controller.mm b/chrome/browser/ui/cocoa/html_dialog_window_controller.mm
new file mode 100644
index 0000000..aa219b28
--- /dev/null
+++ b/chrome/browser/ui/cocoa/html_dialog_window_controller.mm
@@ -0,0 +1,293 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/html_dialog_window_controller.h"
+
+#include "app/keyboard_codes.h"
+#include "base/logging.h"
+#include "base/scoped_nsobject.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/dom_ui/html_dialog_ui.h"
+#include "chrome/browser/dom_ui/html_dialog_tab_contents_delegate.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#import "chrome/browser/ui/cocoa/browser_command_executor.h"
+#import "chrome/browser/ui/cocoa/chrome_event_processing_window.h"
+#include "chrome/common/native_web_keyboard_event.h"
+#include "gfx/size.h"
+#include "ipc/ipc_message.h"
+
+// Thin bridge that routes notifications to
+// HtmlDialogWindowController's member variables.
+class HtmlDialogWindowDelegateBridge : public HtmlDialogUIDelegate,
+ public HtmlDialogTabContentsDelegate {
+public:
+ // All parameters must be non-NULL/non-nil.
+ HtmlDialogWindowDelegateBridge(HtmlDialogWindowController* controller,
+ Profile* profile,
+ HtmlDialogUIDelegate* delegate);
+
+ virtual ~HtmlDialogWindowDelegateBridge();
+
+ // Called when the window is directly closed, e.g. from the close
+ // button or from an accelerator.
+ void WindowControllerClosed();
+
+ // HtmlDialogUIDelegate declarations.
+ virtual bool IsDialogModal() const;
+ virtual std::wstring GetDialogTitle() const;
+ virtual GURL GetDialogContentURL() const;
+ virtual void GetDOMMessageHandlers(
+ std::vector<DOMMessageHandler*>* handlers) const;
+ virtual void GetDialogSize(gfx::Size* size) const;
+ virtual std::string GetDialogArgs() const;
+ virtual void OnDialogClosed(const std::string& json_retval);
+ virtual void OnCloseContents(TabContents* source, bool* out_close_dialog) { }
+ virtual bool ShouldShowDialogTitle() const { return true; }
+
+ // HtmlDialogTabContentsDelegate declarations.
+ virtual void MoveContents(TabContents* source, const gfx::Rect& pos);
+ virtual void ToolbarSizeChanged(TabContents* source, bool is_animating);
+ virtual void HandleKeyboardEvent(const NativeWebKeyboardEvent& event);
+
+private:
+ HtmlDialogWindowController* controller_; // weak
+ HtmlDialogUIDelegate* delegate_; // weak, owned by controller_
+
+ // Calls delegate_'s OnDialogClosed() exactly once, nulling it out
+ // afterwards so that no other HtmlDialogUIDelegate calls are sent
+ // to it. Returns whether or not the OnDialogClosed() was actually
+ // called on the delegate.
+ bool DelegateOnDialogClosed(const std::string& json_retval);
+
+ DISALLOW_COPY_AND_ASSIGN(HtmlDialogWindowDelegateBridge);
+};
+
+// ChromeEventProcessingWindow expects its controller to implement the
+// BrowserCommandExecutor protocol.
+@interface HtmlDialogWindowController (InternalAPI) <BrowserCommandExecutor>
+
+// BrowserCommandExecutor methods.
+- (void)executeCommand:(int)command;
+
+@end
+
+namespace html_dialog_window_controller {
+
+gfx::NativeWindow ShowHtmlDialog(
+ HtmlDialogUIDelegate* delegate, Profile* profile) {
+ return [HtmlDialogWindowController showHtmlDialog:delegate profile:profile];
+}
+
+} // namespace html_dialog_window_controller
+
+HtmlDialogWindowDelegateBridge::HtmlDialogWindowDelegateBridge(
+ HtmlDialogWindowController* controller, Profile* profile,
+ HtmlDialogUIDelegate* delegate)
+ : HtmlDialogTabContentsDelegate(profile),
+ controller_(controller), delegate_(delegate) {
+ DCHECK(controller_);
+ DCHECK(delegate_);
+}
+
+HtmlDialogWindowDelegateBridge::~HtmlDialogWindowDelegateBridge() {}
+
+void HtmlDialogWindowDelegateBridge::WindowControllerClosed() {
+ Detach();
+ controller_ = nil;
+ DelegateOnDialogClosed("");
+}
+
+bool HtmlDialogWindowDelegateBridge::DelegateOnDialogClosed(
+ const std::string& json_retval) {
+ if (delegate_) {
+ HtmlDialogUIDelegate* real_delegate = delegate_;
+ delegate_ = NULL;
+ real_delegate->OnDialogClosed(json_retval);
+ return true;
+ }
+ return false;
+}
+
+// HtmlDialogUIDelegate definitions.
+
+// All of these functions check for NULL first since delegate_ is set
+// to NULL when the window is closed.
+
+bool HtmlDialogWindowDelegateBridge::IsDialogModal() const {
+ // TODO(akalin): Support modal dialog boxes.
+ if (delegate_ && delegate_->IsDialogModal()) {
+ LOG(WARNING) << "Modal HTML dialogs are not supported yet";
+ }
+ return false;
+}
+
+std::wstring HtmlDialogWindowDelegateBridge::GetDialogTitle() const {
+ return delegate_ ? delegate_->GetDialogTitle() : L"";
+}
+
+GURL HtmlDialogWindowDelegateBridge::GetDialogContentURL() const {
+ return delegate_ ? delegate_->GetDialogContentURL() : GURL();
+}
+
+void HtmlDialogWindowDelegateBridge::GetDOMMessageHandlers(
+ std::vector<DOMMessageHandler*>* handlers) const {
+ if (delegate_) {
+ delegate_->GetDOMMessageHandlers(handlers);
+ } else {
+ // TODO(akalin): Add this clause in the windows version. Also
+ // make sure that everything expects handlers to be non-NULL and
+ // document it.
+ handlers->clear();
+ }
+}
+
+void HtmlDialogWindowDelegateBridge::GetDialogSize(gfx::Size* size) const {
+ if (delegate_) {
+ delegate_->GetDialogSize(size);
+ } else {
+ *size = gfx::Size();
+ }
+}
+
+std::string HtmlDialogWindowDelegateBridge::GetDialogArgs() const {
+ return delegate_ ? delegate_->GetDialogArgs() : "";
+}
+
+void HtmlDialogWindowDelegateBridge::OnDialogClosed(
+ const std::string& json_retval) {
+ Detach();
+ // [controller_ close] should be called at most once, too.
+ if (DelegateOnDialogClosed(json_retval)) {
+ [controller_ close];
+ }
+ controller_ = nil;
+}
+
+void HtmlDialogWindowDelegateBridge::MoveContents(TabContents* source,
+ const gfx::Rect& pos) {
+ // TODO(akalin): Actually set the window bounds.
+}
+
+void HtmlDialogWindowDelegateBridge::ToolbarSizeChanged(
+ TabContents* source, bool is_animating) {
+ // TODO(akalin): Figure out what to do here.
+}
+
+// A simplified version of BrowserWindowCocoa::HandleKeyboardEvent().
+// We don't handle global keyboard shortcuts here, but that's fine since
+// they're all browser-specific. (This may change in the future.)
+void HtmlDialogWindowDelegateBridge::HandleKeyboardEvent(
+ const NativeWebKeyboardEvent& event) {
+ if (event.skip_in_browser || event.type == NativeWebKeyboardEvent::Char)
+ return;
+
+ // Close ourselves if the user hits Esc or Command-. . The normal
+ // way to do this is to implement (void)cancel:(int)sender, but
+ // since we handle keyboard events ourselves we can't do that.
+ //
+ // According to experiments, hitting Esc works regardless of the
+ // presence of other modifiers (as long as it's not an app-level
+ // shortcut, e.g. Commmand-Esc for Front Row) but no other modifiers
+ // can be present for Command-. to work.
+ //
+ // TODO(thakis): It would be nice to get cancel: to work somehow.
+ // Bug: http://code.google.com/p/chromium/issues/detail?id=32828 .
+ if (event.type == NativeWebKeyboardEvent::RawKeyDown &&
+ ((event.windowsKeyCode == app::VKEY_ESCAPE) ||
+ (event.windowsKeyCode == app::VKEY_OEM_PERIOD &&
+ event.modifiers == NativeWebKeyboardEvent::MetaKey))) {
+ [controller_ close];
+ return;
+ }
+
+ ChromeEventProcessingWindow* event_window =
+ static_cast<ChromeEventProcessingWindow*>([controller_ window]);
+ DCHECK([event_window isKindOfClass:[ChromeEventProcessingWindow class]]);
+ [event_window redispatchKeyEvent:event.os_event];
+}
+
+@implementation HtmlDialogWindowController (InternalAPI)
+
+// This gets called whenever a chrome-specific keyboard shortcut is performed
+// in the HTML dialog window. We simply swallow all those events.
+- (void)executeCommand:(int)command {}
+
+@end
+
+@implementation HtmlDialogWindowController
+
+// NOTE(akalin): We'll probably have to add the parentWindow parameter back
+// in once we implement modal dialogs.
+
++ (NSWindow*)showHtmlDialog:(HtmlDialogUIDelegate*)delegate
+ profile:(Profile*)profile {
+ HtmlDialogWindowController* htmlDialogWindowController =
+ [[HtmlDialogWindowController alloc] initWithDelegate:delegate
+ profile:profile];
+ [htmlDialogWindowController loadDialogContents];
+ [htmlDialogWindowController showWindow:nil];
+ return [htmlDialogWindowController window];
+}
+
+- (id)initWithDelegate:(HtmlDialogUIDelegate*)delegate
+ profile:(Profile*)profile {
+ DCHECK(delegate);
+ DCHECK(profile);
+
+ gfx::Size dialogSize;
+ delegate->GetDialogSize(&dialogSize);
+ NSRect dialogRect = NSMakeRect(0, 0, dialogSize.width(), dialogSize.height());
+ // TODO(akalin): Make the window resizable (but with the minimum size being
+ // dialog_size and always on top (but not modal) to match the Windows
+ // behavior. On the other hand, the fact that HTML dialogs on Windows
+ // are resizable could just be an accident. Investigate futher...
+ NSUInteger style = NSTitledWindowMask | NSClosableWindowMask;
+ scoped_nsobject<ChromeEventProcessingWindow> window(
+ [[ChromeEventProcessingWindow alloc]
+ initWithContentRect:dialogRect
+ styleMask:style
+ backing:NSBackingStoreBuffered
+ defer:YES]);
+ if (!window.get()) {
+ return nil;
+ }
+ self = [super initWithWindow:window];
+ if (!self) {
+ return nil;
+ }
+ [window setWindowController:self];
+ [window setDelegate:self];
+ [window setTitle:base::SysWideToNSString(delegate->GetDialogTitle())];
+ [window center];
+ delegate_.reset(new HtmlDialogWindowDelegateBridge(self, profile, delegate));
+ return self;
+}
+
+- (void)loadDialogContents {
+ tabContents_.reset(new TabContents(
+ delegate_->profile(), NULL, MSG_ROUTING_NONE, NULL, NULL));
+ [[self window] setContentView:tabContents_->GetNativeView()];
+ tabContents_->set_delegate(delegate_.get());
+
+ // This must be done before loading the page; see the comments in
+ // HtmlDialogUI.
+ HtmlDialogUI::GetPropertyAccessor().SetProperty(tabContents_->property_bag(),
+ delegate_.get());
+
+ tabContents_->controller().LoadURL(delegate_->GetDialogContentURL(),
+ GURL(), PageTransition::START_PAGE);
+
+ // TODO(akalin): add accelerator for ESC to close the dialog box.
+ //
+ // TODO(akalin): Figure out why implementing (void)cancel:(id)sender
+ // to do the above doesn't work.
+}
+
+- (void)windowWillClose:(NSNotification*)notification {
+ delegate_->WindowControllerClosed();
+ [self autorelease];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/html_dialog_window_controller_cppsafe.h b/chrome/browser/ui/cocoa/html_dialog_window_controller_cppsafe.h
new file mode 100644
index 0000000..dd3b809
--- /dev/null
+++ b/chrome/browser/ui/cocoa/html_dialog_window_controller_cppsafe.h
@@ -0,0 +1,32 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_HTML_DIALOG_WINDOW_CONTROLLER_CPPSAFE_H_
+#define CHROME_BROWSER_UI_COCOA_HTML_DIALOG_WINDOW_CONTROLLER_CPPSAFE_H_
+#pragma once
+
+#include "gfx/native_widget_types.h"
+
+// We declare this in a separate file that is safe for including in C++ code.
+
+// TODO(akalin): It would be nice if there were a platform-agnostic way to
+// create a browser-independent HTML dialog. However, this would require
+// some invasive changes on the Windows/Linux side. Remove this file once
+// We have this platform-agnostic API.
+
+namespace html_dialog_window_controller {
+
+// Creates and shows an HtmlDialogWindowController with the given
+// delegate and profile. The window is automatically destroyed when it is
+// closed. Returns the created window.
+//
+// Make sure to use the returned window only when you know it is safe
+// to do so, i.e. before OnDialogClosed() is called on the delegate.
+gfx::NativeWindow ShowHtmlDialog(
+ HtmlDialogUIDelegate* delegate, Profile* profile);
+
+} // namespace html_dialog_window_controller
+
+#endif // CHROME_BROWSER_UI_COCOA_HTML_DIALOG_WINDOW_CONTROLLER_CPPSAFE_H_
+
diff --git a/chrome/browser/ui/cocoa/html_dialog_window_controller_unittest.mm b/chrome/browser/ui/cocoa/html_dialog_window_controller_unittest.mm
new file mode 100644
index 0000000..7bbeb31
--- /dev/null
+++ b/chrome/browser/ui/cocoa/html_dialog_window_controller_unittest.mm
@@ -0,0 +1,94 @@
+// 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/ui/cocoa/html_dialog_window_controller.h"
+
+#include <string>
+#include <vector>
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/mac/scoped_nsautorelease_pool.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/dom_ui/dom_ui.h"
+#include "chrome/browser/dom_ui/html_dialog_ui.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/test/browser_with_test_window_test.h"
+#include "chrome/test/testing_profile.h"
+#include "gfx/size.h"
+#include "googleurl/src/gurl.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+class MockDelegate : public HtmlDialogUIDelegate {
+public:
+ MOCK_CONST_METHOD0(IsDialogModal, bool());
+ MOCK_CONST_METHOD0(GetDialogTitle, std::wstring());
+ MOCK_CONST_METHOD0(GetDialogContentURL, GURL());
+ MOCK_CONST_METHOD1(GetDOMMessageHandlers,
+ void(std::vector<DOMMessageHandler*>*));
+ MOCK_CONST_METHOD1(GetDialogSize, void(gfx::Size*));
+ MOCK_CONST_METHOD0(GetDialogArgs, std::string());
+ MOCK_METHOD1(OnDialogClosed, void(const std::string& json_retval));
+ MOCK_METHOD2(OnCloseContents,
+ void(TabContents* source, bool* out_close_dialog));
+ MOCK_CONST_METHOD0(ShouldShowDialogTitle, bool());
+};
+
+class HtmlDialogWindowControllerTest : public BrowserWithTestWindowTest {
+ public:
+ virtual void SetUp() {
+ BrowserWithTestWindowTest::SetUp();
+ CocoaTest::BootstrapCocoa();
+ title_ = L"Mock Title";
+ size_ = gfx::Size(50, 100);
+ gurl_ = GURL("");
+ }
+
+ protected:
+ std::wstring title_;
+ gfx::Size size_;
+ GURL gurl_;
+
+ // Order here is important.
+ MockDelegate delegate_;
+};
+
+using ::testing::_;
+using ::testing::Return;
+using ::testing::SetArgumentPointee;
+
+// TODO(akalin): We can't test much more than the below without a real browser.
+// In particular, GetDOMMessageHandlers() and GetDialogArgs() are never called.
+// This should be fixed.
+
+TEST_F(HtmlDialogWindowControllerTest, showDialog) {
+ // We want to make sure html_dialog_window_controller below gets
+ // destroyed before delegate_, so we specify our own autorelease pool.
+ //
+ // TODO(dmaclach): Remove this once
+ // http://code.google.com/p/chromium/issues/detail?id=26133 is fixed.
+ base::mac::ScopedNSAutoreleasePool release_pool;
+
+ EXPECT_CALL(delegate_, GetDialogTitle())
+ .WillOnce(Return(title_));
+ EXPECT_CALL(delegate_, GetDialogSize(_))
+ .WillOnce(SetArgumentPointee<0>(size_));
+ EXPECT_CALL(delegate_, GetDialogContentURL())
+ .WillOnce(Return(gurl_));
+ EXPECT_CALL(delegate_, OnDialogClosed(_))
+ .Times(1);
+
+ HtmlDialogWindowController* html_dialog_window_controller =
+ [[HtmlDialogWindowController alloc] initWithDelegate:&delegate_
+ profile:profile()];
+
+ [html_dialog_window_controller loadDialogContents];
+ [html_dialog_window_controller showWindow:nil];
+ [html_dialog_window_controller close];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/hung_renderer_controller.h b/chrome/browser/ui/cocoa/hung_renderer_controller.h
new file mode 100644
index 0000000..0d45c0f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/hung_renderer_controller.h
@@ -0,0 +1,76 @@
+// 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_UI_COCOA_HUNG_RENDERER_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_HUNG_RENDERER_CONTROLLER_H_
+#pragma once
+
+// A controller for the Mac hung renderer dialog window. Only one
+// instance of this controller can exist at any time, although a given
+// controller is destroyed when its window is closed.
+//
+// The dialog itself displays a list of frozen tabs, all of which
+// share a render process. Since there can only be a single dialog
+// open at a time, if showForTabContents is called for a different
+// tab, the dialog is repurposed to show a warning for the new tab.
+//
+// The caller is required to call endForTabContents before deleting
+// any TabContents object.
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/cocoa_protocols_mac.h"
+#import "base/scoped_nsobject.h"
+
+@class MultiKeyEquivalentButton;
+class TabContents;
+
+@interface HungRendererController : NSWindowController<NSTableViewDataSource> {
+ @private
+ IBOutlet MultiKeyEquivalentButton* waitButton_;
+ IBOutlet NSButton* killButton_;
+ IBOutlet NSTableView* tableView_;
+ IBOutlet NSImageView* imageView_;
+ IBOutlet NSTextField* messageView_;
+
+ // The TabContents for which this dialog is open. Should never be
+ // NULL while this dialog is open.
+ TabContents* hungContents_;
+
+ // Backing data for |tableView_|. Titles of each TabContents that
+ // shares a renderer process with |hungContents_|.
+ scoped_nsobject<NSArray> hungTitles_;
+
+ // Favicons of each TabContents that shares a renderer process with
+ // |hungContents_|.
+ scoped_nsobject<NSArray> hungFavicons_;
+}
+
+// Kills the hung renderers.
+- (IBAction)kill:(id)sender;
+
+// Waits longer for the renderers to respond.
+- (IBAction)wait:(id)sender;
+
+// Modifies the dialog to show a warning for the given tab contents.
+// The dialog will contain a list of all tabs that share a renderer
+// process with |contents|. The caller must not delete any tab
+// contents without first calling endForTabContents.
+- (void)showForTabContents:(TabContents*)contents;
+
+// Notifies the dialog that |contents| is either responsive or closed.
+// If |contents| shares the same render process as the tab contents
+// this dialog was created for, this function will close the dialog.
+// If |contents| has a different process, this function does nothing.
+- (void)endForTabContents:(TabContents*)contents;
+
+@end // HungRendererController
+
+
+@interface HungRendererController (JustForTesting)
+- (NSButton*)killButton;
+- (MultiKeyEquivalentButton*)waitButton;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_HUNG_RENDERER_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/hung_renderer_controller.mm b/chrome/browser/ui/cocoa/hung_renderer_controller.mm
new file mode 100644
index 0000000..130a3f7
--- /dev/null
+++ b/chrome/browser/ui/cocoa/hung_renderer_controller.mm
@@ -0,0 +1,203 @@
+// 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/ui/cocoa/hung_renderer_controller.h"
+
+#import <Cocoa/Cocoa.h>
+
+#include "app/resource_bundle.h"
+#include "app/l10n_util_mac.h"
+#include "base/mac_util.h"
+#include "base/process_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/browser_list.h"
+#include "chrome/browser/hung_renderer_dialog.h"
+#include "chrome/browser/renderer_host/render_process_host.h"
+#include "chrome/browser/renderer_host/render_view_host.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#import "chrome/browser/ui/cocoa/multi_key_equivalent_button.h"
+#include "chrome/common/logging_chrome.h"
+#include "chrome/common/result_codes.h"
+#include "grit/chromium_strings.h"
+#include "grit/app_resources.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+namespace {
+// We only support showing one of these at a time per app. The
+// controller owns itself and is released when its window is closed.
+HungRendererController* g_instance = NULL;
+} // end namespace
+
+@implementation HungRendererController
+
+- (id)initWithWindowNibName:(NSString*)nibName {
+ NSString* nibpath = [mac_util::MainAppBundle() pathForResource:nibName
+ ofType:@"nib"];
+ self = [super initWithWindowNibPath:nibpath owner:self];
+ if (self) {
+ [tableView_ setDataSource:self];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ DCHECK(!g_instance);
+ [tableView_ setDataSource:nil];
+ [super dealloc];
+}
+
+- (void)awakeFromNib {
+ // Load in the image
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ NSImage* backgroundImage = rb.GetNativeImageNamed(IDR_FROZEN_TAB_ICON);
+ DCHECK(backgroundImage);
+ [imageView_ setImage:backgroundImage];
+
+ // Make the message fit.
+ CGFloat messageShift =
+ [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:messageView_];
+
+ // Move the graphic up to be top even with the message.
+ NSRect graphicFrame = [imageView_ frame];
+ graphicFrame.origin.y += messageShift;
+ [imageView_ setFrame:graphicFrame];
+
+ // Make the window taller to fit everything.
+ NSSize windowDelta = NSMakeSize(0, messageShift);
+ [GTMUILocalizerAndLayoutTweaker
+ resizeWindowWithoutAutoResizingSubViews:[self window]
+ delta:windowDelta];
+
+ // Make the "wait" button respond to additional keys. By setting this to
+ // @"\e", it will respond to both Esc and Command-. (period).
+ KeyEquivalentAndModifierMask key;
+ key.charCode = @"\e";
+ [waitButton_ addKeyEquivalent:key];
+}
+
+- (IBAction)kill:(id)sender {
+ if (hungContents_)
+ base::KillProcess(hungContents_->GetRenderProcessHost()->GetHandle(),
+ ResultCodes::HUNG, false);
+ // Cannot call performClose:, because the close button is disabled.
+ [self close];
+}
+
+- (IBAction)wait:(id)sender {
+ if (hungContents_ && hungContents_->render_view_host())
+ hungContents_->render_view_host()->RestartHangMonitorTimeout();
+ // Cannot call performClose:, because the close button is disabled.
+ [self close];
+}
+
+- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView {
+ return [hungTitles_ count];
+}
+
+- (id)tableView:(NSTableView*)aTableView
+ objectValueForTableColumn:(NSTableColumn*)column
+ row:(NSInteger)rowIndex {
+ return [NSNumber numberWithInt:NSOffState];
+}
+
+- (NSCell*)tableView:(NSTableView*)tableView
+ dataCellForTableColumn:(NSTableColumn*)tableColumn
+ row:(NSInteger)rowIndex {
+ NSCell* cell = [tableColumn dataCellForRow:rowIndex];
+
+ if ([[tableColumn identifier] isEqualToString:@"title"]) {
+ DCHECK([cell isKindOfClass:[NSButtonCell class]]);
+ NSButtonCell* buttonCell = static_cast<NSButtonCell*>(cell);
+ [buttonCell setTitle:[hungTitles_ objectAtIndex:rowIndex]];
+ [buttonCell setImage:[hungFavicons_ objectAtIndex:rowIndex]];
+ [buttonCell setRefusesFirstResponder:YES]; // Don't push in like a button.
+ [buttonCell setHighlightsBy:NSNoCellMask];
+ }
+ return cell;
+}
+
+- (void)windowWillClose:(NSNotification*)notification {
+ // We have to reset g_instance before autoreleasing the window,
+ // because we want to avoid reusing the same dialog if someone calls
+ // hung_renderer_dialog::ShowForTabContents() between the autorelease
+ // call and the actual dealloc.
+ g_instance = nil;
+
+ [self autorelease];
+}
+
+- (void)showForTabContents:(TabContents*)contents {
+ DCHECK(contents);
+ hungContents_ = contents;
+ scoped_nsobject<NSMutableArray> titles([[NSMutableArray alloc] init]);
+ scoped_nsobject<NSMutableArray> favicons([[NSMutableArray alloc] init]);
+ for (TabContentsIterator it; !it.done(); ++it) {
+ if (it->GetRenderProcessHost() == hungContents_->GetRenderProcessHost()) {
+ string16 title = (*it)->GetTitle();
+ if (title.empty())
+ title = TabContents::GetDefaultTitle();
+ [titles addObject:base::SysUTF16ToNSString(title)];
+
+ // TabContents can return a null SkBitmap if it has no favicon. If this
+ // happens, use the default favicon.
+ const SkBitmap& bitmap = it->GetFavIcon();
+ if (!bitmap.isNull()) {
+ [favicons addObject:gfx::SkBitmapToNSImage(bitmap)];
+ } else {
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ [favicons addObject:rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON)];
+ }
+ }
+ }
+ hungTitles_.reset([titles copy]);
+ hungFavicons_.reset([favicons copy]);
+ [tableView_ reloadData];
+
+ [[self window] center];
+ [self showWindow:self];
+}
+
+- (void)endForTabContents:(TabContents*)contents {
+ DCHECK(contents);
+ DCHECK(hungContents_);
+ if (hungContents_ && hungContents_->GetRenderProcessHost() ==
+ contents->GetRenderProcessHost()) {
+ // Cannot call performClose:, because the close button is disabled.
+ [self close];
+ }
+}
+
+@end
+
+@implementation HungRendererController (JustForTesting)
+- (NSButton*)killButton {
+ return killButton_;
+}
+
+- (MultiKeyEquivalentButton*)waitButton {
+ return waitButton_;
+}
+@end
+
+namespace hung_renderer_dialog {
+
+void ShowForTabContents(TabContents* contents) {
+ if (!logging::DialogsAreSuppressed()) {
+ if (!g_instance)
+ g_instance = [[HungRendererController alloc]
+ initWithWindowNibName:@"HungRendererDialog"];
+ [g_instance showForTabContents:contents];
+ }
+}
+
+// static
+void HideForTabContents(TabContents* contents) {
+ if (!logging::DialogsAreSuppressed() && g_instance)
+ [g_instance endForTabContents:contents];
+}
+
+} // namespace hung_renderer_dialog
diff --git a/chrome/browser/ui/cocoa/hung_renderer_controller_unittest.mm b/chrome/browser/ui/cocoa/hung_renderer_controller_unittest.mm
new file mode 100644
index 0000000..e03becf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/hung_renderer_controller_unittest.mm
@@ -0,0 +1,51 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/scoped_nsobject.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/hung_renderer_controller.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class HungRendererControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ hung_renderer_controller_ = [[HungRendererController alloc]
+ initWithWindowNibName:@"HungRendererDialog"];
+ }
+ HungRendererController* hung_renderer_controller_; // owned by its window
+};
+
+TEST_F(HungRendererControllerTest, TestShowAndClose) {
+ // Doesn't test much functionality-wise, but makes sure we can
+ // display and tear down a window.
+ [hung_renderer_controller_ showWindow:nil];
+ // Cannot call performClose:, because the close button is disabled.
+ [hung_renderer_controller_ close];
+}
+
+TEST_F(HungRendererControllerTest, TestKillButton) {
+ // We can't test killing a process because we have no running
+ // process to kill, but we can make sure that pressing the kill
+ // button closes the window.
+ [hung_renderer_controller_ showWindow:nil];
+ [[hung_renderer_controller_ killButton] performClick:nil];
+}
+
+TEST_F(HungRendererControllerTest, TestWaitButton) {
+ // We can't test waiting because we have no running process to wait
+ // for, but we can make sure that pressing the wait button closes
+ // the window.
+ [hung_renderer_controller_ showWindow:nil];
+ [[hung_renderer_controller_ waitButton] performClick:nil];
+}
+
+} // namespace
+
diff --git a/chrome/browser/ui/cocoa/hyperlink_button_cell.h b/chrome/browser/ui/cocoa/hyperlink_button_cell.h
new file mode 100644
index 0000000..c4d27ff
--- /dev/null
+++ b/chrome/browser/ui/cocoa/hyperlink_button_cell.h
@@ -0,0 +1,25 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+#include "base/scoped_nsobject.h"
+
+// A HyperlinkButtonCell is used to create an NSButton that looks and acts
+// like a hyperlink. The default styling is to look like blue, underlined text
+// and to have the pointingHand cursor on mouse over.
+//
+// To use in Interface Builder:
+// 1. Drag out an NSButton.
+// 2. Double click on the button so you have the cell component selected.
+// 3. In the Identity panel of the inspector, set the custom class to this.
+// 4. In the Attributes panel, change the Bezel to Square.
+// 5. In the Size panel, set the Height to 16.
+@interface HyperlinkButtonCell : NSButtonCell {
+ scoped_nsobject<NSColor> textColor_;
+}
+@property (nonatomic, retain) NSColor* textColor;
+
++ (NSColor*)defaultTextColor;
+
+@end
diff --git a/chrome/browser/ui/cocoa/hyperlink_button_cell.mm b/chrome/browser/ui/cocoa/hyperlink_button_cell.mm
new file mode 100644
index 0000000..c8bb93e8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/hyperlink_button_cell.mm
@@ -0,0 +1,116 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/hyperlink_button_cell.h"
+
+@interface HyperlinkButtonCell (Private)
+- (NSDictionary*)linkAttributres;
+- (void)customizeButtonCell;
+@end
+
+@implementation HyperlinkButtonCell
+@dynamic textColor;
+
++ (NSColor*)defaultTextColor {
+ return [NSColor blueColor];
+}
+
+// Designated initializer.
+- (id)init {
+ if ((self = [super init])) {
+ [self customizeButtonCell];
+ }
+ return self;
+}
+
+// Initializer called when the cell is loaded from the NIB.
+- (id)initWithCoder:(NSCoder*)aDecoder {
+ if ((self = [super initWithCoder:aDecoder])) {
+ [self customizeButtonCell];
+ }
+ return self;
+}
+
+// Initializer for code-based creation.
+- (id)initTextCell:(NSString*)title {
+ if ((self = [super initTextCell:title])) {
+ [self customizeButtonCell];
+ }
+ return self;
+}
+
+// Because an NSButtonCell has multiple initializers, this method performs the
+// common cell customization code.
+- (void)customizeButtonCell {
+ [self setBordered:NO];
+ [self setTextColor:[HyperlinkButtonCell defaultTextColor]];
+
+ CGFloat fontSize = [NSFont systemFontSizeForControlSize:[self controlSize]];
+ NSFont* font = [NSFont controlContentFontOfSize:fontSize];
+ [self setFont:font];
+
+ // Do not change button appearance when we are pushed.
+ // TODO(rsesek): Change text color to red?
+ [self setHighlightsBy:NSNoCellMask];
+
+ // We need to set this so that we can override |-mouseEntered:| and
+ // |-mouseExited:| to change the cursor style on hover states.
+ [self setShowsBorderOnlyWhileMouseInside:YES];
+}
+
+- (void)setControlSize:(NSControlSize)size {
+ [super setControlSize:size];
+ [self customizeButtonCell]; // recompute |font|.
+}
+
+// Creates the NSDictionary of attributes for the attributed string.
+- (NSDictionary*)linkAttributes {
+ NSUInteger underlineMask = NSUnderlinePatternSolid | NSUnderlineStyleSingle;
+ scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
+ [[NSParagraphStyle defaultParagraphStyle] mutableCopy]);
+ [paragraphStyle setAlignment:[self alignment]];
+
+ return [NSDictionary dictionaryWithObjectsAndKeys:
+ [self textColor], NSForegroundColorAttributeName,
+ [NSNumber numberWithInt:underlineMask], NSUnderlineStyleAttributeName,
+ [self font], NSFontAttributeName,
+ [NSCursor pointingHandCursor], NSCursorAttributeName,
+ paragraphStyle.get(), NSParagraphStyleAttributeName,
+ nil
+ ];
+}
+
+// Override the drawing for the cell so that the custom style attributes
+// can always be applied and so that ellipses will appear when appropriate.
+- (NSRect)drawTitle:(NSAttributedString*)title
+ withFrame:(NSRect)frame
+ inView:(NSView*)controlView {
+ NSDictionary* linkAttributes = [self linkAttributes];
+ NSString* plainTitle = [title string];
+ [plainTitle drawWithRect:frame
+ options:(NSStringDrawingUsesLineFragmentOrigin |
+ NSStringDrawingTruncatesLastVisibleLine)
+ attributes:linkAttributes];
+ return frame;
+}
+
+// Override the default behavior to draw the border. Instead, change the cursor.
+- (void)mouseEntered:(NSEvent*)event {
+ [[NSCursor pointingHandCursor] push];
+}
+
+- (void)mouseExited:(NSEvent*)event {
+ [NSCursor pop];
+}
+
+// Setters and getters.
+- (NSColor*)textColor {
+ return textColor_.get();
+}
+
+- (void)setTextColor:(NSColor*)color {
+ textColor_.reset([color retain]);
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/hyperlink_button_cell_unittest.mm b/chrome/browser/ui/cocoa/hyperlink_button_cell_unittest.mm
new file mode 100644
index 0000000..0adae14
--- /dev/null
+++ b/chrome/browser/ui/cocoa/hyperlink_button_cell_unittest.mm
@@ -0,0 +1,75 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/hyperlink_button_cell.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class HyperlinkButtonCellTest : public CocoaTest {
+ public:
+ HyperlinkButtonCellTest() {
+ NSRect frame = NSMakeRect(0, 0, 50, 30);
+ scoped_nsobject<NSButton> view([[NSButton alloc] initWithFrame:frame]);
+ view_ = view.get();
+ scoped_nsobject<HyperlinkButtonCell> cell(
+ [[HyperlinkButtonCell alloc] initTextCell:@"Testing"]);
+ cell_ = cell.get();
+ [view_ setCell:cell_];
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ void TestCellCustomization(HyperlinkButtonCell* cell) {
+ EXPECT_FALSE([cell isBordered]);
+ EXPECT_EQ(NSNoCellMask, [cell_ highlightsBy]);
+ EXPECT_TRUE([cell showsBorderOnlyWhileMouseInside]);
+ EXPECT_TRUE([cell textColor]);
+ }
+
+ NSButton* view_;
+ HyperlinkButtonCell* cell_;
+};
+
+TEST_VIEW(HyperlinkButtonCellTest, view_)
+
+// Tests the three designated intializers.
+TEST_F(HyperlinkButtonCellTest, Initializers) {
+ TestCellCustomization(cell_); // |-initTextFrame:|
+ scoped_nsobject<HyperlinkButtonCell> cell([[HyperlinkButtonCell alloc] init]);
+ TestCellCustomization(cell.get());
+
+ // Need to create a dummy archiver to test |-initWithCoder:|.
+ NSData* emptyData = [NSKeyedArchiver archivedDataWithRootObject:@""];
+ NSCoder* coder =
+ [[[NSKeyedUnarchiver alloc] initForReadingWithData:emptyData] autorelease];
+ cell.reset([[HyperlinkButtonCell alloc] initWithCoder:coder]);
+ TestCellCustomization(cell);
+}
+
+// Test set color.
+TEST_F(HyperlinkButtonCellTest, SetTextColor) {
+ NSColor* textColor = [NSColor redColor];
+ EXPECT_NE(textColor, [cell_ textColor]);
+ [cell_ setTextColor:textColor];
+ EXPECT_EQ(textColor, [cell_ textColor]);
+}
+
+// Test mouse events.
+// TODO(rsesek): See if we can synthesize mouse events to more accurately
+// test this.
+TEST_F(HyperlinkButtonCellTest, MouseHover) {
+ [[NSCursor disappearingItemCursor] push]; // Set a known state.
+ [cell_ mouseEntered:nil];
+ EXPECT_EQ([NSCursor pointingHandCursor], [NSCursor currentCursor]);
+ [cell_ mouseExited:nil];
+ EXPECT_EQ([NSCursor disappearingItemCursor], [NSCursor currentCursor]);
+ [NSCursor pop];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/image_utils.h b/chrome/browser/ui/cocoa/image_utils.h
new file mode 100644
index 0000000..5c43828
--- /dev/null
+++ b/chrome/browser/ui/cocoa/image_utils.h
@@ -0,0 +1,26 @@
+// 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_UI_COCOA_IMAGE_UTILS_H_
+#define CHROME_BROWSER_UI_COCOA_IMAGE_UTILS_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+@interface NSImage (FlippedAdditions)
+
+// Works like |-drawInRect:fromRect:operation:fraction:|, except that
+// if |neverFlipped| is |YES|, and the context is flipped, the a
+// transform is applied to flip it again before drawing the image.
+//
+// Compare to the 10.6 method
+// |-drawInRect:fromRect:operation:fraction:respectFlipped:hints:|.
+- (void)drawInRect:(NSRect)dstRect
+ fromRect:(NSRect)srcRect
+ operation:(NSCompositingOperation)op
+ fraction:(CGFloat)requestedAlpha
+ neverFlipped:(BOOL)neverFlipped;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_IMAGE_UTILS_H_
diff --git a/chrome/browser/ui/cocoa/image_utils.mm b/chrome/browser/ui/cocoa/image_utils.mm
new file mode 100644
index 0000000..b2883ff
--- /dev/null
+++ b/chrome/browser/ui/cocoa/image_utils.mm
@@ -0,0 +1,37 @@
+// 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/ui/cocoa/image_utils.h"
+
+@implementation NSImage (FlippedAdditions)
+
+- (void)drawInRect:(NSRect)dstRect
+ fromRect:(NSRect)srcRect
+ operation:(NSCompositingOperation)op
+ fraction:(CGFloat)requestedAlpha
+ neverFlipped:(BOOL)neverFlipped {
+ NSAffineTransform *transform = nil;
+
+ // Flip drawing and adjust the origin to make the image come out
+ // right.
+ if (neverFlipped && [[NSGraphicsContext currentContext] isFlipped]) {
+ transform = [NSAffineTransform transform];
+ [transform scaleXBy:1.0 yBy:-1.0];
+ [transform concat];
+
+ // The lower edge of the image is as far from the origin as the
+ // upper edge was, plus it's on the other side of the origin.
+ dstRect.origin.y -= NSMaxY(dstRect) + NSMinY(dstRect);
+ }
+
+ [self drawInRect:dstRect
+ fromRect:srcRect
+ operation:op
+ fraction:requestedAlpha];
+
+ // Flip drawing back, if needed.
+ [transform concat];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/image_utils_unittest.mm b/chrome/browser/ui/cocoa/image_utils_unittest.mm
new file mode 100644
index 0000000..d13b33b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/image_utils_unittest.mm
@@ -0,0 +1,138 @@
+// 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/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/image_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+@interface ImageUtilsTestView : NSView {
+ @private
+ // Determine whether the view is flipped.
+ BOOL isFlipped_;
+
+ // Determines whether to draw using the new method with
+ // |neverFlipped:|.
+ BOOL useNeverFlipped_;
+
+ // Passed to |neverFlipped:| when drawing |image_|.
+ BOOL neverFlipped_;
+
+ scoped_nsobject<NSImage> image_;
+}
+@property(assign, nonatomic) BOOL isFlipped;
+@property(assign, nonatomic) BOOL useNeverFlipped;
+@property(assign, nonatomic) BOOL neverFlipped;
+@end
+
+@implementation ImageUtilsTestView
+@synthesize isFlipped = isFlipped_;
+@synthesize useNeverFlipped = useNeverFlipped_;
+@synthesize neverFlipped = neverFlipped_;
+
+- (id)initWithFrame:(NSRect)rect {
+ self = [super initWithFrame:rect];
+ if (self) {
+ rect = NSInsetRect(rect, 5.0, 5.0);
+ rect.origin = NSZeroPoint;
+ const NSSize imageSize = NSInsetRect(rect, 5.0, 5.0).size;
+ image_.reset([[NSImage alloc] initWithSize:imageSize]);
+
+ NSBezierPath* path = [NSBezierPath bezierPath];
+ [path moveToPoint:NSMakePoint(NSMinX(rect), NSMinY(rect))];
+ [path lineToPoint:NSMakePoint(NSMinX(rect), NSMaxY(rect))];
+ [path lineToPoint:NSMakePoint(NSMaxX(rect), NSMinY(rect))];
+ [path closePath];
+
+ [image_ lockFocus];
+ [[NSColor blueColor] setFill];
+ [path fill];
+ [image_ unlockFocus];
+ }
+ return self;
+}
+
+- (void)drawRect:(NSRect)rect {
+ NSBezierPath* path = [NSBezierPath bezierPath];
+ [path moveToPoint:NSMakePoint(NSMinX(rect), NSMinY(rect))];
+ [path lineToPoint:NSMakePoint(NSMinX(rect), NSMaxY(rect))];
+ [path lineToPoint:NSMakePoint(NSMaxX(rect), NSMinY(rect))];
+ [path closePath];
+
+ [[NSColor redColor] setFill];
+ [path fill];
+
+ rect = NSInsetRect(rect, 5.0, 5.0);
+ rect = NSOffsetRect(rect, 2.0, 2.0);
+
+ if (useNeverFlipped_) {
+ [image_ drawInRect:rect
+ fromRect:NSZeroRect
+ operation:NSCompositeCopy
+ fraction:1.0
+ neverFlipped:neverFlipped_];
+ } else {
+ [image_ drawInRect:rect
+ fromRect:NSZeroRect
+ operation:NSCompositeCopy
+ fraction:1.0];
+ }
+}
+
+@end
+
+namespace {
+
+class ImageUtilTest : public CocoaTest {
+ public:
+ ImageUtilTest() {
+ const NSRect frame = NSMakeRect(0, 0, 300, 100);
+ scoped_nsobject<ImageUtilsTestView> view(
+ [[ImageUtilsTestView alloc] initWithFrame: frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ NSData* SnapshotView() {
+ [view_ display];
+
+ const NSRect bounds = [view_ bounds];
+
+ [view_ lockFocus];
+ scoped_nsobject<NSBitmapImageRep> bitmap(
+ [[NSBitmapImageRep alloc] initWithFocusedViewRect:bounds]);
+ [view_ unlockFocus];
+
+ return [bitmap TIFFRepresentation];
+ }
+
+ NSData* SnapshotViewBase() {
+ [view_ setUseNeverFlipped:NO];
+ return SnapshotView();
+ }
+
+ NSData* SnapshotViewNeverFlipped(BOOL neverFlipped) {
+ [view_ setUseNeverFlipped:YES];
+ [view_ setNeverFlipped:neverFlipped];
+ return SnapshotView();
+ }
+
+ ImageUtilsTestView* view_;
+};
+
+TEST_F(ImageUtilTest, Test) {
+ // When not flipped, both drawing methods return the same data.
+ [view_ setIsFlipped:NO];
+ NSData* baseSnapshotData = SnapshotViewBase();
+ EXPECT_TRUE([baseSnapshotData isEqualToData:SnapshotViewNeverFlipped(YES)]);
+ EXPECT_TRUE([baseSnapshotData isEqualToData:SnapshotViewNeverFlipped(NO)]);
+
+ // When flipped, there's only a difference when the context flip is
+ // not being respected.
+ [view_ setIsFlipped:YES];
+ baseSnapshotData = SnapshotViewBase();
+ EXPECT_FALSE([baseSnapshotData isEqualToData:SnapshotViewNeverFlipped(YES)]);
+ EXPECT_TRUE([baseSnapshotData isEqualToData:SnapshotViewNeverFlipped(NO)]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/import_progress_dialog.h b/chrome/browser/ui/cocoa/import_progress_dialog.h
new file mode 100644
index 0000000..c1b405b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/import_progress_dialog.h
@@ -0,0 +1,102 @@
+// 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_IMPORT_PROGRESS_DIALOG_H_
+#define CHROME_BROWSER_IMPORT_PROGRESS_DIALOG_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/importer/importer.h"
+#include "chrome/browser/importer/importer_data_types.h"
+
+class ImporterObserverBridge;
+
+// Class that acts as a controller for the dialog that shows progress for an
+// import operation.
+// Lifetime: This object is responsible for deleting itself.
+@interface ImportProgressDialogController : NSWindowController {
+ scoped_ptr<ImporterObserverBridge> import_host_observer_bridge_;
+ ImporterHost* importer_host_; // (weak)
+ ImportObserver* observer_; // (weak)
+
+ // Strings bound to static labels in the UI dialog.
+ NSString* explanatory_text_;
+ NSString* favorites_status_text_;
+ NSString* search_status_text_;
+ NSString* saved_password_status_text_;
+ NSString* history_status_text_;
+
+ // Bound to the color of the status text (this is the easiest way to disable
+ // progress items that aren't supported by the current browser we're importing
+ // from).
+ NSColor* favorites_import_enabled_;
+ NSColor* search_import_enabled_;
+ NSColor* password_import_enabled_;
+ NSColor* history_import_enabled_;
+
+ // Placeholders for "Importing..." and "Done" text.
+ NSString* progress_text_;
+ NSString* done_text_;
+}
+
+// Cancel button calls this.
+- (IBAction)cancel:(id)sender;
+
+// Closes the dialog.
+- (void)closeDialog;
+
+// Methods called by importer_host via ImporterObserverBridge.
+- (void)ImportItemStarted:(importer::ImportItem)item;
+- (void)ImportItemEnded:(importer::ImportItem)item;
+- (void)ImportEnded;
+
+@property (nonatomic, retain) NSString* explanatoryText;
+@property (nonatomic, retain) NSString* favoritesStatusText;
+@property (nonatomic, retain) NSString* searchStatusText;
+@property (nonatomic, retain) NSString* savedPasswordStatusText;
+@property (nonatomic, retain) NSString* historyStatusText;
+
+@property (nonatomic, retain) NSColor* favoritesImportEnabled;
+@property (nonatomic, retain) NSColor* searchImportEnabled;
+@property (nonatomic, retain) NSColor* passwordImportEnabled;
+@property (nonatomic, retain) NSColor* historyImportEnabled;
+
+@end
+
+// C++ -> objc bridge for import status notifications.
+class ImporterObserverBridge : public ImporterHost::Observer {
+ public:
+ ImporterObserverBridge(ImportProgressDialogController* owner)
+ : owner_(owner) {}
+ virtual ~ImporterObserverBridge() {}
+
+ // Invoked when data for the specified item is about to be collected.
+ virtual void ImportItemStarted(importer::ImportItem item) {
+ [owner_ ImportItemStarted:item];
+ }
+
+ // Invoked when data for the specified item has been collected from the
+ // source profile and is now ready for further processing.
+ virtual void ImportItemEnded(importer::ImportItem item) {
+ [owner_ ImportItemEnded:item];
+ }
+
+ // Invoked when the import begins.
+ virtual void ImportStarted() {
+ // Not needed for out of process import.
+ }
+
+ // Invoked when the source profile has been imported.
+ virtual void ImportEnded() {
+ [owner_ ImportEnded];
+ }
+
+ private:
+ ImportProgressDialogController* owner_;
+
+ DISALLOW_COPY_AND_ASSIGN(ImporterObserverBridge);
+};
+
+#endif // CHROME_BROWSER_IMPORT_PROGRESS_DIALOG_H_
diff --git a/chrome/browser/ui/cocoa/import_progress_dialog.mm b/chrome/browser/ui/cocoa/import_progress_dialog.mm
new file mode 100644
index 0000000..6ec0ed5
--- /dev/null
+++ b/chrome/browser/ui/cocoa/import_progress_dialog.mm
@@ -0,0 +1,192 @@
+// 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/ui/cocoa/import_progress_dialog.h"
+
+#include "app/l10n_util.h"
+#include "app/l10n_util_mac.h"
+#include "base/logging.h"
+#include "base/mac_util.h"
+#include "base/message_loop.h"
+#import "base/scoped_nsobject.h"
+#import "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+
+namespace {
+
+// Convert ImportItem enum into the name of the ImportProgressDialogController
+// property corresponding to the text for that item, this makes the code to
+// change the values for said properties much more readable.
+NSString* keyForImportItem(importer::ImportItem item) {
+ switch(item) {
+ case importer::HISTORY:
+ return @"historyStatusText";
+ case importer::FAVORITES:
+ return @"favoritesStatusText";
+ case importer::PASSWORDS:
+ return @"savedPasswordStatusText";
+ case importer::SEARCH_ENGINES:
+ return @"searchStatusText";
+ default:
+ DCHECK(false);
+ break;
+ }
+ return nil;
+}
+
+} // namespace
+
+@implementation ImportProgressDialogController
+
+@synthesize explanatoryText = explanatory_text_;
+@synthesize favoritesStatusText = favorites_status_text_;
+@synthesize searchStatusText = search_status_text_;
+@synthesize savedPasswordStatusText = saved_password_status_text_;
+@synthesize historyStatusText = history_status_text_;
+
+@synthesize favoritesImportEnabled = favorites_import_enabled_;
+@synthesize searchImportEnabled = search_import_enabled_;
+@synthesize passwordImportEnabled = password_import_enabled_;
+@synthesize historyImportEnabled = history_import_enabled_;
+
+- (id)initWithImporterHost:(ImporterHost*)host
+ browserName:(string16)browserName
+ observer:(ImportObserver*)observer
+ itemsEnabled:(int16)items {
+ NSString* nib_path =
+ [mac_util::MainAppBundle() pathForResource:@"ImportProgressDialog"
+ ofType:@"nib"];
+ self = [super initWithWindowNibPath:nib_path owner:self];
+ if (self != nil) {
+ importer_host_ = host;
+ observer_ = observer;
+ import_host_observer_bridge_.reset(new ImporterObserverBridge(self));
+ importer_host_->SetObserver(import_host_observer_bridge_.get());
+
+ string16 productName = l10n_util::GetStringUTF16(IDS_PRODUCT_NAME);
+ NSString* explanatory_text = l10n_util::GetNSStringF(
+ IDS_IMPORT_PROGRESS_EXPLANATORY_TEXT_MAC,
+ productName,
+ browserName);
+ [self setExplanatoryText:explanatory_text];
+
+ progress_text_ =
+ [l10n_util::GetNSStringWithFixup(IDS_IMPORT_IMPORTING_PROGRESS_TEXT_MAC)
+ retain];
+ done_text_ =
+ [l10n_util::GetNSStringWithFixup(IDS_IMPORT_IMPORTING_DONE_TEXT_MAC)
+ retain];
+
+ // Enable/disable item titles.
+ NSColor* disabled = [NSColor disabledControlTextColor];
+ NSColor* active = [NSColor textColor];
+ [self setFavoritesImportEnabled:items & importer::FAVORITES ? active :
+ disabled];
+ [self setSearchImportEnabled:items & importer::SEARCH_ENGINES ? active :
+ disabled];
+ [self setPasswordImportEnabled:items & importer::PASSWORDS ? active :
+ disabled];
+ [self setHistoryImportEnabled:items & importer::HISTORY ? active :
+ disabled];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [explanatory_text_ release];
+ [favorites_status_text_ release];
+ [search_status_text_ release];
+ [saved_password_status_text_ release];
+ [history_status_text_ release];
+
+ [favorites_import_enabled_ release];
+ [search_import_enabled_ release];
+ [password_import_enabled_ release];
+ [history_import_enabled_ release];
+
+ [progress_text_ release];
+ [done_text_ release];
+
+ [super dealloc];
+}
+
+- (IBAction)showWindow:(id)sender {
+ NSWindow* win = [self window];
+ [win center];
+ [super showWindow:nil];
+}
+
+- (void)closeDialog {
+ if ([[self window] isVisible]) {
+ [[self window] close];
+ }
+}
+
+- (IBAction)cancel:(id)sender {
+ // The ImporterHost will notify import_host_observer_bridge_ that import has
+ // ended, which will trigger the ImportEnded method, in which this object is
+ // released.
+ importer_host_->Cancel();
+}
+
+- (void)ImportItemStarted:(importer::ImportItem)item {
+ [self setValue:progress_text_ forKey:keyForImportItem(item)];
+}
+
+- (void)ImportItemEnded:(importer::ImportItem)item {
+ [self setValue:done_text_ forKey:keyForImportItem(item)];
+}
+
+- (void)ImportEnded {
+ importer_host_->SetObserver(NULL);
+ if (observer_)
+ observer_->ImportComplete();
+ [self closeDialog];
+ [self release];
+
+ // Break out of modal event loop.
+ [NSApp stopModal];
+}
+
+@end
+
+void StartImportingWithUI(gfx::NativeWindow parent_window,
+ uint16 items,
+ ImporterHost* coordinator,
+ const importer::ProfileInfo& source_profile,
+ Profile* target_profile,
+ ImportObserver* observer,
+ bool first_run) {
+ DCHECK(items != 0);
+
+ // Retrieve name of browser we're importing from and do a little dance to
+ // convert wstring -> string16.
+ string16 import_browser_name = WideToUTF16Hack(source_profile.description);
+
+ // progress_dialog_ is responsible for deleting itself.
+ ImportProgressDialogController* progress_dialog_ =
+ [[ImportProgressDialogController alloc]
+ initWithImporterHost:coordinator
+ browserName:import_browser_name
+ observer:observer
+ itemsEnabled:items];
+ // Call is async.
+ coordinator->StartImportSettings(source_profile, target_profile, items,
+ new ProfileWriter(target_profile),
+ first_run);
+
+ // Display the window while spinning a message loop.
+ // For details on why we need a modal message loop see http://crbug.com/19169
+ NSWindow* progress_window = [progress_dialog_ window];
+ NSModalSession session = [NSApp beginModalSessionForWindow:progress_window];
+ [progress_dialog_ showWindow:nil];
+ while (true) {
+ if ([NSApp runModalSession:session] != NSRunContinuesResponse)
+ break;
+ MessageLoop::current()->RunAllPending();
+ }
+ [NSApp endModalSession:session];
+}
diff --git a/chrome/browser/ui/cocoa/import_settings_dialog.h b/chrome/browser/ui/cocoa/import_settings_dialog.h
new file mode 100644
index 0000000..2b84ff9
--- /dev/null
+++ b/chrome/browser/ui/cocoa/import_settings_dialog.h
@@ -0,0 +1,98 @@
+// 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_UI_COCOA_IMPORT_SETTINGS_DIALOG_H_
+#define CHROME_BROWSER_UI_COCOA_IMPORT_SETTINGS_DIALOG_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/importer/importer.h"
+
+class Profile;
+
+// Controller for the Import Bookmarks and Settings dialog. This controller
+// automatically autoreleases itself when its associated dialog is dismissed.
+@interface ImportSettingsDialogController : NSWindowController {
+ @private
+ NSWindow* parentWindow_; // weak
+ Profile* profile_; // weak
+ scoped_ptr<ImporterList> importerList_;
+ scoped_nsobject<NSArray> sourceBrowsersList_;
+ NSUInteger sourceBrowserIndex_;
+ // The following are all bound via the properties below.
+ BOOL importHistory_;
+ BOOL importFavorites_;
+ BOOL importPasswords_;
+ BOOL importSearchEngines_;
+ BOOL historyAvailable_;
+ BOOL favoritesAvailable_;
+ BOOL passwordsAvailable_;
+ BOOL searchEnginesAvailable_;
+}
+
+// Show the import settings window. Window is displayed as an app modal dialog.
+// If the dialog is already being displayed, this method whill return with
+// no error.
++ (void)showImportSettingsDialogForProfile:(Profile*)profile;
+
+// Called when the "Import" button is pressed.
+- (IBAction)ok:(id)sender;
+
+// Cancel button calls this.
+- (IBAction)cancel:(id)sender;
+
+// An array of ImportSettingsProfiles, provide the list of browser profiles
+// available for importing. Bound to the Browser List array controller.
+- (NSArray*)sourceBrowsersList;
+
+// Properties for bindings.
+@property(assign, nonatomic) NSUInteger sourceBrowserIndex;
+@property(assign, readonly, nonatomic) BOOL importSomething;
+// Bindings for the value of the import checkboxes.
+@property(assign, nonatomic) BOOL importHistory;
+@property(assign, nonatomic) BOOL importFavorites;
+@property(assign, nonatomic) BOOL importPasswords;
+@property(assign, nonatomic) BOOL importSearchEngines;
+// Bindings for enabling/disabling the checkboxes.
+@property(assign, readonly, nonatomic) BOOL historyAvailable;
+@property(assign, readonly, nonatomic) BOOL favoritesAvailable;
+@property(assign, readonly, nonatomic) BOOL passwordsAvailable;
+@property(assign, readonly, nonatomic) BOOL searchEnginesAvailable;
+
+@end
+
+@interface ImportSettingsDialogController (TestingAPI)
+
+// Initialize by providing an array of profile dictionaries. Exposed for
+// unit testing but also called by -[initWithProfile:].
+- (id)initWithProfiles:(NSArray*)profiles;
+
+// Return selected services to import as mapped by the ImportItem enum.
+- (uint16)servicesToImport;
+
+@end
+
+// Utility class used as array elements for sourceBrowsersList, above.
+@interface ImportSettingsProfile : NSObject {
+ @private
+ NSString* browserName_;
+ uint16 services_; // Services as defined by enum ImportItem.
+}
+
+// Convenience creator. |services| is a bitfield of enum ImportItems.
++ (id)importSettingsProfileWithBrowserName:(NSString*)browserName
+ services:(uint16)services;
+
+// Designated initializer. |services| is a bitfield of enum ImportItems.
+- (id)initWithBrowserName:(NSString*)browserName
+ services:(uint16)services; // Bitfield of enum ImportItems.
+
+@property(copy, nonatomic) NSString* browserName;
+@property(assign, nonatomic) uint16 services; // Bitfield of enum ImportItems.
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_IMPORT_SETTINGS_DIALOG_H_
diff --git a/chrome/browser/ui/cocoa/import_settings_dialog.mm b/chrome/browser/ui/cocoa/import_settings_dialog.mm
new file mode 100644
index 0000000..59a9140
--- /dev/null
+++ b/chrome/browser/ui/cocoa/import_settings_dialog.mm
@@ -0,0 +1,245 @@
+// 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/ui/cocoa/import_settings_dialog.h"
+
+#include "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/importer/importer_data_types.h"
+#include "chrome/browser/importer/importer_list.h"
+#include "chrome/browser/profile.h"
+
+namespace {
+
+bool importSettingsDialogVisible = false;
+
+} // namespace
+
+@interface ImportSettingsDialogController ()
+
+@property(assign, readwrite, nonatomic) BOOL historyAvailable;
+@property(assign, readwrite, nonatomic) BOOL favoritesAvailable;
+@property(assign, readwrite, nonatomic) BOOL passwordsAvailable;
+@property(assign, readwrite, nonatomic) BOOL searchEnginesAvailable;
+
+@end
+
+@implementation ImportSettingsProfile
+
+@synthesize browserName = browserName_;
+@synthesize services = services_;
+
++ (id)importSettingsProfileWithBrowserName:(NSString*)browserName
+ services:(uint16)services {
+ id settingsProfile = [[[ImportSettingsProfile alloc]
+ initWithBrowserName:browserName
+ services:services] autorelease];
+ return settingsProfile;
+}
+
+- (id)initWithBrowserName:(NSString*)browserName
+ services:(uint16)services {
+ DCHECK(browserName && services);
+ if ((self = [super init])) {
+ if (browserName && services != 0) {
+ browserName_ = [browserName retain];
+ services_ = services;
+ } else {
+ [self release];
+ self = nil;
+ }
+ }
+ return self;
+}
+
+- (id)init {
+ NOTREACHED(); // Should never be called.
+ return [self initWithBrowserName:NULL services:0];
+}
+
+- (void)dealloc {
+ [browserName_ release];
+ [super dealloc];
+}
+
+@end
+
+@interface ImportSettingsDialogController (Private)
+
+// Initialize the dialog controller with either the default profile or
+// the profile for the current browser.
+- (id)initWithProfile:(Profile*)profile;
+
+// Present the app modal dialog.
+- (void)runModalDialog;
+
+// Close the modal dialog.
+- (void)closeDialog;
+
+@end
+
+@implementation ImportSettingsDialogController
+
+@synthesize sourceBrowserIndex = sourceBrowserIndex_;
+@synthesize importHistory = importHistory_;
+@synthesize importFavorites = importFavorites_;
+@synthesize importPasswords = importPasswords_;
+@synthesize importSearchEngines = importSearchEngines_;
+@synthesize historyAvailable = historyAvailable_;
+@synthesize favoritesAvailable = favoritesAvailable_;
+@synthesize passwordsAvailable = passwordsAvailable_;
+@synthesize searchEnginesAvailable = searchEnginesAvailable_;
+
+// Set bindings dependencies for importSomething property.
++ (NSSet*)keyPathsForValuesAffectingImportSomething {
+ return [NSSet setWithObjects:@"importHistory", @"importFavorites",
+ @"importPasswords", @"importSearchEngines", nil];
+}
+
++ (void)showImportSettingsDialogForProfile:(Profile*)profile {
+ // Don't display if already visible.
+ if (importSettingsDialogVisible)
+ return;
+ ImportSettingsDialogController* controller =
+ [[ImportSettingsDialogController alloc] initWithProfile:profile];
+ [controller runModalDialog];
+}
+
+- (id)initWithProfile:(Profile*)profile {
+ // Collect profile information (profile name and the services which can
+ // be imported from each) into an array of ImportSettingsProfile which
+ // are bound to the Browser List array controller and the popup name
+ // presentation. The services element is used to indirectly control
+ // checkbox enabling.
+ importerList_.reset(new ImporterList);
+ ImporterList& importerList(*(importerList_.get()));
+ importerList.DetectSourceProfiles();
+ int profilesCount = importerList.GetAvailableProfileCount();
+ // There shoule be at least the default profile so this should never be zero.
+ DCHECK(profilesCount > 0);
+ NSMutableArray* browserProfiles =
+ [NSMutableArray arrayWithCapacity:profilesCount];
+ for (int i = 0; i < profilesCount; ++i) {
+ const importer::ProfileInfo& sourceProfile =
+ importerList.GetSourceProfileInfoAt(i);
+ NSString* browserName =
+ base::SysWideToNSString(sourceProfile.description);
+ uint16 browserServices = sourceProfile.services_supported;
+ ImportSettingsProfile* settingsProfile =
+ [ImportSettingsProfile
+ importSettingsProfileWithBrowserName:browserName
+ services:browserServices];
+ [browserProfiles addObject:settingsProfile];
+ }
+ if ((self = [self initWithProfiles:browserProfiles])) {
+ profile_ = profile;
+ }
+ return self;
+}
+
+- (id)initWithProfiles:(NSArray*)profiles {
+ NSString* nibpath =
+ [mac_util::MainAppBundle() pathForResource:@"ImportSettingsDialog"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ sourceBrowsersList_.reset([profiles retain]);
+ // Create and initialize an importerList_ when running unit tests.
+ if (!importerList_.get()) {
+ importerList_.reset(new ImporterList);
+ ImporterList& importerList(*(importerList_.get()));
+ importerList.DetectSourceProfiles();
+ }
+ }
+ return self;
+}
+
+- (id)init {
+ return [self initWithProfile:NULL];
+}
+
+- (void)awakeFromNib {
+ // Force an update of the checkbox enabled states.
+ [self setSourceBrowserIndex:0];
+}
+
+// Run application modal.
+- (void)runModalDialog {
+ importSettingsDialogVisible = true;
+ [NSApp runModalForWindow:[self window]];
+}
+
+- (IBAction)ok:(id)sender {
+ [self closeDialog];
+ const importer::ProfileInfo& sourceProfile =
+ importerList_.get()->GetSourceProfileInfoAt([self sourceBrowserIndex]);
+ uint16 items = sourceProfile.services_supported;
+ uint16 servicesToImport = items & [self servicesToImport];
+ if (servicesToImport) {
+ if (profile_) {
+ ImporterHost* importerHost = new ExternalProcessImporterHost;
+ // Note that a side effect of the following call is to cause the
+ // importerHost to be disposed once the import has completed.
+ StartImportingWithUI(nil, servicesToImport, importerHost,
+ sourceProfile, profile_, nil, false);
+ }
+ } else {
+ LOG(WARNING) << "There were no settings to import from '"
+ << sourceProfile.description << "'.";
+ }
+}
+
+- (IBAction)cancel:(id)sender {
+ [self closeDialog];
+}
+
+- (void)closeDialog {
+ importSettingsDialogVisible = false;
+ [[self window] orderOut:self];
+ [NSApp stopModal];
+ [self autorelease];
+}
+
+#pragma mark Accessors
+
+- (NSArray*)sourceBrowsersList {
+ return sourceBrowsersList_.get();
+}
+
+// Accessor which cascades selected-browser changes into a re-evaluation of the
+// available services and the associated checkbox enable and checked states.
+- (void)setSourceBrowserIndex:(NSUInteger)browserIndex {
+ sourceBrowserIndex_ = browserIndex;
+ ImportSettingsProfile* profile =
+ [sourceBrowsersList_.get() objectAtIndex:browserIndex];
+ uint16 items = [profile services];
+ [self setHistoryAvailable:(items & importer::HISTORY) ? YES : NO];
+ [self setImportHistory:[self historyAvailable]];
+ [self setFavoritesAvailable:(items & importer::FAVORITES) ? YES : NO];
+ [self setImportFavorites:[self favoritesAvailable]];
+ [self setPasswordsAvailable:(items & importer::PASSWORDS) ? YES : NO];
+ [self setImportPasswords:[self passwordsAvailable]];
+ [self setSearchEnginesAvailable:(items & importer::SEARCH_ENGINES) ?
+ YES : NO];
+ [self setImportSearchEngines:[self searchEnginesAvailable]];
+}
+
+- (uint16)servicesToImport {
+ uint16 servicesToImport = 0;
+ if ([self importHistory]) servicesToImport |= importer::HISTORY;
+ if ([self importFavorites]) servicesToImport |= importer::FAVORITES;
+ if ([self importPasswords]) servicesToImport |= importer::PASSWORDS;
+ if ([self importSearchEngines]) servicesToImport |=
+ importer::SEARCH_ENGINES;
+ return servicesToImport;
+}
+
+// KVO accessor which returns YES if at least one of the services
+// provided by the selected profile has been marked for importing
+// and bound to the OK button's enable property.
+- (BOOL)importSomething {
+ return [self importHistory] || [self importFavorites] ||
+ [self importPasswords] || [self importSearchEngines];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/import_settings_dialog_unittest.mm b/chrome/browser/ui/cocoa/import_settings_dialog_unittest.mm
new file mode 100644
index 0000000..f9399c2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/import_settings_dialog_unittest.mm
@@ -0,0 +1,130 @@
+// 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"
+#include "chrome/browser/importer/importer.h"
+#import "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/import_settings_dialog.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+using importer::HISTORY;
+using importer::FAVORITES;
+using importer::COOKIES;
+using importer::PASSWORDS;
+using importer::SEARCH_ENGINES;
+using importer::NONE;
+
+class ImportSettingsDialogTest : public CocoaTest {
+ public:
+ ImportSettingsDialogController* controller_;
+
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ unsigned int safariServices =
+ HISTORY | FAVORITES | COOKIES | PASSWORDS | SEARCH_ENGINES;
+ ImportSettingsProfile* mockSafari =
+ [ImportSettingsProfile
+ importSettingsProfileWithBrowserName:@"MockSafari"
+ services:safariServices];
+ unsigned int firefoxServices = HISTORY | FAVORITES | COOKIES | PASSWORDS;
+ ImportSettingsProfile* mockFirefox =
+ [ImportSettingsProfile
+ importSettingsProfileWithBrowserName:@"MockFirefox"
+ services:firefoxServices];
+ unsigned int caminoServices = HISTORY | COOKIES | SEARCH_ENGINES;
+ ImportSettingsProfile* mockCamino =
+ [ImportSettingsProfile
+ importSettingsProfileWithBrowserName:@"MockCamino"
+ services:caminoServices];
+ NSArray* browsers = [NSArray arrayWithObjects:
+ mockSafari, mockFirefox, mockCamino, nil];
+ controller_ = [[ImportSettingsDialogController alloc]
+ initWithProfiles:browsers];
+ }
+
+ virtual void TearDown() {
+ controller_ = NULL;
+ CocoaTest::TearDown();
+ }
+};
+
+TEST_F(ImportSettingsDialogTest, CancelDialog) {
+ [controller_ cancel:nil];
+}
+
+TEST_F(ImportSettingsDialogTest, ChooseVariousBrowsers) {
+ // Initial choice should already be MockSafari with all items enabled.
+ [controller_ setSourceBrowserIndex:0];
+ EXPECT_TRUE([controller_ importHistory]);
+ EXPECT_TRUE([controller_ historyAvailable]);
+ EXPECT_TRUE([controller_ importFavorites]);
+ EXPECT_TRUE([controller_ favoritesAvailable]);
+ EXPECT_TRUE([controller_ importPasswords]);
+ EXPECT_TRUE([controller_ passwordsAvailable]);
+ EXPECT_TRUE([controller_ importSearchEngines]);
+ EXPECT_TRUE([controller_ searchEnginesAvailable]);
+ EXPECT_EQ(HISTORY | FAVORITES | PASSWORDS | SEARCH_ENGINES,
+ [controller_ servicesToImport]);
+
+ // Next choice we test is MockCamino.
+ [controller_ setSourceBrowserIndex:2];
+ EXPECT_TRUE([controller_ importHistory]);
+ EXPECT_TRUE([controller_ historyAvailable]);
+ EXPECT_FALSE([controller_ importFavorites]);
+ EXPECT_FALSE([controller_ favoritesAvailable]);
+ EXPECT_FALSE([controller_ importPasswords]);
+ EXPECT_FALSE([controller_ passwordsAvailable]);
+ EXPECT_TRUE([controller_ importSearchEngines]);
+ EXPECT_TRUE([controller_ searchEnginesAvailable]);
+ EXPECT_EQ(HISTORY | SEARCH_ENGINES, [controller_ servicesToImport]);
+
+ // Next choice we test is MockFirefox.
+ [controller_ setSourceBrowserIndex:1];
+ EXPECT_TRUE([controller_ importHistory]);
+ EXPECT_TRUE([controller_ historyAvailable]);
+ EXPECT_TRUE([controller_ importFavorites]);
+ EXPECT_TRUE([controller_ favoritesAvailable]);
+ EXPECT_TRUE([controller_ importPasswords]);
+ EXPECT_TRUE([controller_ passwordsAvailable]);
+ EXPECT_FALSE([controller_ importSearchEngines]);
+ EXPECT_FALSE([controller_ searchEnginesAvailable]);
+ EXPECT_EQ(HISTORY | FAVORITES | PASSWORDS, [controller_ servicesToImport]);
+
+ [controller_ cancel:nil];
+}
+
+TEST_F(ImportSettingsDialogTest, SetVariousSettings) {
+ // Leave the choice MockSafari, but toggle the settings.
+ [controller_ setImportHistory:NO];
+ [controller_ setImportFavorites:NO];
+ [controller_ setImportPasswords:NO];
+ [controller_ setImportSearchEngines:NO];
+ EXPECT_EQ(NONE, [controller_ servicesToImport]);
+ EXPECT_FALSE([controller_ importSomething]);
+
+ [controller_ setImportHistory:YES];
+ EXPECT_EQ(HISTORY, [controller_ servicesToImport]);
+ EXPECT_TRUE([controller_ importSomething]);
+
+ [controller_ setImportHistory:NO];
+ [controller_ setImportFavorites:YES];
+ EXPECT_EQ(FAVORITES, [controller_ servicesToImport]);
+ EXPECT_TRUE([controller_ importSomething]);
+ [controller_ setImportFavorites:NO];
+
+ [controller_ setImportPasswords:YES];
+ EXPECT_EQ(PASSWORDS, [controller_ servicesToImport]);
+ EXPECT_TRUE([controller_ importSomething]);
+
+ [controller_ setImportPasswords:NO];
+ [controller_ setImportSearchEngines:YES];
+ EXPECT_EQ(SEARCH_ENGINES, [controller_ servicesToImport]);
+ EXPECT_TRUE([controller_ importSomething]);
+
+ [controller_ cancel:nil];
+}
diff --git a/chrome/browser/ui/cocoa/importer_lock_dialog.h b/chrome/browser/ui/cocoa/importer_lock_dialog.h
new file mode 100644
index 0000000..ade74e2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/importer_lock_dialog.h
@@ -0,0 +1,21 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_IMPORTER_LOCK_DIALOG_H_
+#define CHROME_BROWSER_UI_COCOA_IMPORTER_LOCK_DIALOG_H_
+#pragma once
+
+class ImporterHost;
+
+namespace ImportLockDialogCocoa {
+
+// This function is called by an ImporterHost, and displays the Firefox profile
+// locked warning by creating a modal NSAlert. On the closing of the alert
+// box, the ImportHost receives a callback with the message either to skip the
+// import, or to try again.
+void ShowWarning(ImporterHost* importer);
+
+}
+
+#endif // CHROME_BROWSER_UI_COCOA_IMPORTER_LOCK_DIALOG_H_
diff --git a/chrome/browser/ui/cocoa/importer_lock_dialog.mm b/chrome/browser/ui/cocoa/importer_lock_dialog.mm
new file mode 100644
index 0000000..cc94ce5
--- /dev/null
+++ b/chrome/browser/ui/cocoa/importer_lock_dialog.mm
@@ -0,0 +1,35 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "importer_lock_dialog.h"
+
+#include "app/l10n_util_mac.h"
+#include "base/mac_util.h"
+#include "base/message_loop.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/importer/importer.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+
+void ImportLockDialogCocoa::ShowWarning(ImporterHost* importer) {
+ scoped_nsobject<NSAlert> lock_alert([[NSAlert alloc] init]);
+ [lock_alert addButtonWithTitle:l10n_util::GetNSStringWithFixup(
+ IDS_IMPORTER_LOCK_OK)];
+ [lock_alert addButtonWithTitle:l10n_util::GetNSStringWithFixup(
+ IDS_IMPORTER_LOCK_CANCEL)];
+ [lock_alert setInformativeText:l10n_util::GetNSStringWithFixup(
+ IDS_IMPORTER_LOCK_TEXT)];
+ [lock_alert setMessageText:l10n_util::GetNSStringWithFixup(
+ IDS_IMPORTER_LOCK_TITLE)];
+
+ if ([lock_alert runModal] == NSAlertFirstButtonReturn) {
+ MessageLoop::current()->PostTask(FROM_HERE, NewRunnableMethod(
+ importer, &ImporterHost::OnLockViewEnd, true));
+ } else {
+ MessageLoop::current()->PostTask(FROM_HERE, NewRunnableMethod(
+ importer, &ImporterHost::OnLockViewEnd, false));
+ }
+}
diff --git a/chrome/browser/ui/cocoa/info_bubble_view.h b/chrome/browser/ui/cocoa/info_bubble_view.h
new file mode 100644
index 0000000..974581b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/info_bubble_view.h
@@ -0,0 +1,50 @@
+// 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_UI_COCOA_INFO_BUBBLE_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_INFO_BUBBLE_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+namespace info_bubble {
+
+const CGFloat kBubbleArrowHeight = 8.0;
+const CGFloat kBubbleArrowWidth = 15.0;
+const CGFloat kBubbleCornerRadius = 8.0;
+const CGFloat kBubbleArrowXOffset = kBubbleArrowWidth + kBubbleCornerRadius;
+
+enum BubbleArrowLocation {
+ kTopLeft,
+ kTopRight,
+};
+
+enum InfoBubbleType {
+ kWhiteInfoBubble,
+ // Gradient bubbles are deprecated, per alcor@google.com. Please use white.
+ kGradientInfoBubble
+};
+
+} // namespace info_bubble
+
+// Content view for a bubble with an arrow showing arbitrary content.
+// This is where nonrectangular drawing happens.
+@interface InfoBubbleView : NSView {
+ @private
+ info_bubble::BubbleArrowLocation arrowLocation_;
+
+ // The type simply is used to determine what sort of background it should
+ // draw.
+ info_bubble::InfoBubbleType bubbleType_;
+}
+
+@property (assign, nonatomic) info_bubble::BubbleArrowLocation arrowLocation;
+@property (assign, nonatomic) info_bubble::InfoBubbleType bubbleType;
+
+// Returns the point location in view coordinates of the tip of the arrow.
+- (NSPoint)arrowTip;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_INFO_BUBBLE_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/info_bubble_view.mm b/chrome/browser/ui/cocoa/info_bubble_view.mm
new file mode 100644
index 0000000..8033cb5
--- /dev/null
+++ b/chrome/browser/ui/cocoa/info_bubble_view.mm
@@ -0,0 +1,103 @@
+// 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/ui/cocoa/info_bubble_view.h"
+
+#include "base/logging.h"
+#include "base/scoped_nsobject.h"
+#import "third_party/GTM/AppKit/GTMNSColor+Luminance.h"
+
+@implementation InfoBubbleView
+
+@synthesize arrowLocation = arrowLocation_;
+@synthesize bubbleType = bubbleType_;
+
+- (id)initWithFrame:(NSRect)frameRect {
+ if ((self = [super initWithFrame:frameRect])) {
+ arrowLocation_ = info_bubble::kTopLeft;
+ bubbleType_ = info_bubble::kWhiteInfoBubble;
+ }
+
+ return self;
+}
+
+- (void)drawRect:(NSRect)rect {
+ // Make room for the border to be seen.
+ NSRect bounds = [self bounds];
+ bounds.size.height -= info_bubble::kBubbleArrowHeight;
+ NSBezierPath* bezier = [NSBezierPath bezierPath];
+ rect.size.height -= info_bubble::kBubbleArrowHeight;
+
+ // Start with a rounded rectangle.
+ [bezier appendBezierPathWithRoundedRect:bounds
+ xRadius:info_bubble::kBubbleCornerRadius
+ yRadius:info_bubble::kBubbleCornerRadius];
+
+ // Add the bubble arrow.
+ CGFloat dX = 0;
+ switch (arrowLocation_) {
+ case info_bubble::kTopLeft:
+ dX = info_bubble::kBubbleArrowXOffset;
+ break;
+ case info_bubble::kTopRight:
+ dX = NSWidth(bounds) - info_bubble::kBubbleArrowXOffset -
+ info_bubble::kBubbleArrowWidth;
+ break;
+ default:
+ NOTREACHED();
+ break;
+ }
+ NSPoint arrowStart = NSMakePoint(NSMinX(bounds), NSMaxY(bounds));
+ arrowStart.x += dX;
+ [bezier moveToPoint:NSMakePoint(arrowStart.x, arrowStart.y)];
+ [bezier lineToPoint:NSMakePoint(arrowStart.x +
+ info_bubble::kBubbleArrowWidth / 2.0,
+ arrowStart.y +
+ info_bubble::kBubbleArrowHeight)];
+ [bezier lineToPoint:NSMakePoint(arrowStart.x + info_bubble::kBubbleArrowWidth,
+ arrowStart.y)];
+ [bezier closePath];
+
+ // Then fill the inside depending on the type of bubble.
+ if (bubbleType_ == info_bubble::kGradientInfoBubble) {
+ NSColor* base_color = [NSColor colorWithCalibratedWhite:0.5 alpha:1.0];
+ NSColor* startColor =
+ [base_color gtm_colorAdjustedFor:GTMColorationLightHighlight
+ faded:YES];
+ NSColor* midColor =
+ [base_color gtm_colorAdjustedFor:GTMColorationLightMidtone
+ faded:YES];
+ NSColor* endColor =
+ [base_color gtm_colorAdjustedFor:GTMColorationLightShadow
+ faded:YES];
+ NSColor* glowColor =
+ [base_color gtm_colorAdjustedFor:GTMColorationLightPenumbra
+ faded:YES];
+
+ scoped_nsobject<NSGradient> gradient(
+ [[NSGradient alloc] initWithColorsAndLocations:startColor, 0.0,
+ midColor, 0.25,
+ endColor, 0.5,
+ glowColor, 0.75,
+ nil]);
+
+ [gradient.get() drawInBezierPath:bezier angle:0.0];
+ } else if (bubbleType_ == info_bubble::kWhiteInfoBubble) {
+ [[NSColor whiteColor] set];
+ [bezier fill];
+ }
+}
+
+- (NSPoint)arrowTip {
+ NSRect bounds = [self bounds];
+ CGFloat tipXOffset =
+ info_bubble::kBubbleArrowXOffset + info_bubble::kBubbleArrowWidth / 2.0;
+ CGFloat xOffset =
+ (arrowLocation_ == info_bubble::kTopRight) ? NSMaxX(bounds) - tipXOffset :
+ NSMinX(bounds) + tipXOffset;
+ NSPoint arrowTip = NSMakePoint(xOffset, NSMaxY(bounds));
+ return arrowTip;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/info_bubble_view_unittest.mm b/chrome/browser/ui/cocoa/info_bubble_view_unittest.mm
new file mode 100644
index 0000000..c3c438d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/info_bubble_view_unittest.mm
@@ -0,0 +1,26 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/info_bubble_view.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+
+namespace {
+
+class InfoBubbleViewTest : public CocoaTest {
+ public:
+ InfoBubbleViewTest() {
+ NSRect frame = NSMakeRect(0, 0, 100, 30);
+ scoped_nsobject<InfoBubbleView> view(
+ [[InfoBubbleView alloc] initWithFrame:frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ InfoBubbleView* view_;
+};
+
+TEST_VIEW(InfoBubbleViewTest, view_);
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/info_bubble_window.h b/chrome/browser/ui/cocoa/info_bubble_window.h
new file mode 100644
index 0000000..38ae44d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/info_bubble_window.h
@@ -0,0 +1,32 @@
+// 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_ptr.h"
+#import "chrome/browser/ui/cocoa/chrome_event_processing_window.h"
+
+class AppNotificationBridge;
+
+// A rounded window with an arrow used for example when you click on the STAR
+// button or that pops up within our first-run UI.
+@interface InfoBubbleWindow : ChromeEventProcessingWindow {
+ @private
+ // Is self in the process of closing.
+ BOOL closing_;
+ // If NO the window will close immediately instead of fading out.
+ // Default YES.
+ BOOL delayOnClose_;
+ // Bridge to proxy Chrome notifications to the window.
+ scoped_ptr<AppNotificationBridge> notificationBridge_;
+}
+
+// Returns YES if the window is in the process of closing.
+// Can't use "windowWillClose" notification because that will be sent
+// after the closing animation has completed.
+- (BOOL)isClosing;
+
+@property (nonatomic) BOOL delayOnClose;
+
+@end
diff --git a/chrome/browser/ui/cocoa/info_bubble_window.mm b/chrome/browser/ui/cocoa/info_bubble_window.mm
new file mode 100644
index 0000000..183cf8a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/info_bubble_window.mm
@@ -0,0 +1,222 @@
+// 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/ui/cocoa/info_bubble_window.h"
+
+#include "base/basictypes.h"
+#include "base/logging.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/common/notification_observer.h"
+#include "chrome/common/notification_registrar.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/notification_type.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+
+namespace {
+const CGFloat kOrderInSlideOffset = 10;
+const NSTimeInterval kOrderInAnimationDuration = 0.2;
+const NSTimeInterval kOrderOutAnimationDuration = 0.15;
+// The minimum representable time interval. This can be used as the value
+// passed to +[NSAnimationContext setDuration:] to stop an in-progress
+// animation as quickly as possible.
+const NSTimeInterval kMinimumTimeInterval =
+ std::numeric_limits<NSTimeInterval>::min();
+}
+
+@interface InfoBubbleWindow(Private)
+- (void)appIsTerminating;
+- (void)finishCloseAfterAnimation;
+@end
+
+// A helper class to proxy app notifications to the window.
+class AppNotificationBridge : public NotificationObserver {
+ public:
+ explicit AppNotificationBridge(InfoBubbleWindow* owner) : owner_(owner) {
+ registrar_.Add(this, NotificationType::APP_TERMINATING,
+ NotificationService::AllSources());
+ }
+
+ // Overridden from NotificationObserver.
+ void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ switch (type.value) {
+ case NotificationType::APP_TERMINATING:
+ [owner_ appIsTerminating];
+ break;
+ default:
+ NOTREACHED() << L"Unexpected notification";
+ }
+ }
+
+ private:
+ // The object we need to inform when we get a notification. Weak. Owns us.
+ InfoBubbleWindow* owner_;
+
+ // Used for registering to receive notifications and automatic clean up.
+ NotificationRegistrar registrar_;
+
+ DISALLOW_COPY_AND_ASSIGN(AppNotificationBridge);
+};
+
+// A delegate object for watching the alphaValue animation on InfoBubbleWindows.
+// An InfoBubbleWindow instance cannot be the delegate for its own animation
+// because CAAnimations retain their delegates, and since the InfoBubbleWindow
+// retains its animations a retain loop would be formed.
+@interface InfoBubbleWindowCloser : NSObject {
+ @private
+ InfoBubbleWindow* window_; // Weak. Window to close.
+}
+- (id)initWithWindow:(InfoBubbleWindow*)window;
+@end
+
+@implementation InfoBubbleWindowCloser
+
+- (id)initWithWindow:(InfoBubbleWindow*)window {
+ if ((self = [super init])) {
+ window_ = window;
+ }
+ return self;
+}
+
+// Callback for the alpha animation. Closes window_ if appropriate.
+- (void)animationDidStop:(CAAnimation*)anim finished:(BOOL)flag {
+ // When alpha reaches zero, close window_.
+ if ([window_ alphaValue] == 0.0) {
+ [window_ finishCloseAfterAnimation];
+ }
+}
+
+@end
+
+
+@implementation InfoBubbleWindow
+
+@synthesize delayOnClose = delayOnClose_;
+
+- (id)initWithContentRect:(NSRect)contentRect
+ styleMask:(NSUInteger)aStyle
+ backing:(NSBackingStoreType)bufferingType
+ defer:(BOOL)flag {
+ if ((self = [super initWithContentRect:contentRect
+ styleMask:NSBorderlessWindowMask
+ backing:bufferingType
+ defer:flag])) {
+ [self setBackgroundColor:[NSColor clearColor]];
+ [self setExcludedFromWindowsMenu:YES];
+ [self setOpaque:NO];
+ [self setHasShadow:YES];
+ delayOnClose_ = YES;
+ notificationBridge_.reset(new AppNotificationBridge(self));
+
+ // Start invisible. Will be made visible when ordered front.
+ [self setAlphaValue:0.0];
+
+ // Set up alphaValue animation so that self is delegate for the animation.
+ // Setting up the delegate is required so that the
+ // animationDidStop:finished: callback can be handled.
+ // Notice that only the alphaValue Animation is replaced in case
+ // superclasses set up animations.
+ CAAnimation* alphaAnimation = [CABasicAnimation animation];
+ scoped_nsobject<InfoBubbleWindowCloser> delegate(
+ [[InfoBubbleWindowCloser alloc] initWithWindow:self]);
+ [alphaAnimation setDelegate:delegate];
+ NSMutableDictionary* animations =
+ [NSMutableDictionary dictionaryWithDictionary:[self animations]];
+ [animations setObject:alphaAnimation forKey:@"alphaValue"];
+ [self setAnimations:animations];
+ }
+ return self;
+}
+
+// According to
+// http://www.cocoabuilder.com/archive/message/cocoa/2006/6/19/165953,
+// NSBorderlessWindowMask windows cannot become key or main. In this
+// case, this is not a desired behavior. As an example, the bubble could have
+// buttons.
+- (BOOL)canBecomeKeyWindow {
+ return YES;
+}
+
+- (void)close {
+ // Block the window from receiving events while it fades out.
+ closing_ = YES;
+
+ if (!delayOnClose_) {
+ [self finishCloseAfterAnimation];
+ } else {
+ // Apply animations to hide self.
+ [NSAnimationContext beginGrouping];
+ [[NSAnimationContext currentContext]
+ gtm_setDuration:kOrderOutAnimationDuration
+ eventMask:NSLeftMouseUpMask];
+ [[self animator] setAlphaValue:0.0];
+ [NSAnimationContext endGrouping];
+ }
+}
+
+// If the app is terminating but the window is still fading out, cancel the
+// animation and close the window to prevent it from leaking.
+// See http://crbug.com/37717
+- (void)appIsTerminating {
+ if (!delayOnClose_)
+ return; // The close has already happened with no Core Animation.
+
+ // Cancel the current animation so that it closes immediately, triggering
+ // |finishCloseAfterAnimation|.
+ [NSAnimationContext beginGrouping];
+ [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
+ [[self animator] setAlphaValue:0.0];
+ [NSAnimationContext endGrouping];
+}
+
+// Called by InfoBubbleWindowCloser when the window is to be really closed
+// after the fading animation is complete.
+- (void)finishCloseAfterAnimation {
+ if (closing_)
+ [super close];
+}
+
+// Adds animation for info bubbles being ordered to the front.
+- (void)orderWindow:(NSWindowOrderingMode)orderingMode
+ relativeTo:(NSInteger)otherWindowNumber {
+ // According to the documentation '0' is the otherWindowNumber when the window
+ // is ordered front.
+ if (orderingMode == NSWindowAbove && otherWindowNumber == 0) {
+ // Order self appropriately assuming that its alpha is zero as set up
+ // in the designated initializer.
+ [super orderWindow:orderingMode relativeTo:otherWindowNumber];
+
+ // Set up frame so it can be adjust down by a few pixels.
+ NSRect frame = [self frame];
+ NSPoint newOrigin = frame.origin;
+ newOrigin.y += kOrderInSlideOffset;
+ [self setFrameOrigin:newOrigin];
+
+ // Apply animations to show and move self.
+ [NSAnimationContext beginGrouping];
+ // The star currently triggers on mouse down, not mouse up.
+ [[NSAnimationContext currentContext]
+ gtm_setDuration:kOrderInAnimationDuration
+ eventMask:NSLeftMouseUpMask|NSLeftMouseDownMask];
+ [[self animator] setAlphaValue:1.0];
+ [[self animator] setFrame:frame display:YES];
+ [NSAnimationContext endGrouping];
+ } else {
+ [super orderWindow:orderingMode relativeTo:otherWindowNumber];
+ }
+}
+
+// If the window is currently animating a close, block all UI events to the
+// window.
+- (void)sendEvent:(NSEvent*)theEvent {
+ if (!closing_)
+ [super sendEvent:theEvent];
+}
+
+- (BOOL)isClosing {
+ return closing_;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/info_bubble_window_unittest.mm b/chrome/browser/ui/cocoa/info_bubble_window_unittest.mm
new file mode 100644
index 0000000..aed5c0a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/info_bubble_window_unittest.mm
@@ -0,0 +1,22 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/scoped_ptr.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/info_bubble_window.h"
+
+class InfoBubbleWindowTest : public CocoaTest {};
+
+TEST_F(InfoBubbleWindowTest, Basics) {
+ InfoBubbleWindow* window =
+ [[InfoBubbleWindow alloc] initWithContentRect:NSMakeRect(0, 0, 10, 10)
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ EXPECT_TRUE([window canBecomeKeyWindow]);
+ EXPECT_FALSE([window canBecomeMainWindow]);
+
+ EXPECT_TRUE([window isExcludedFromWindowsMenu]);
+ [window close];
+}
diff --git a/chrome/browser/ui/cocoa/infobar.h b/chrome/browser/ui/cocoa/infobar.h
new file mode 100644
index 0000000..fc51da7
--- /dev/null
+++ b/chrome/browser/ui/cocoa/infobar.h
@@ -0,0 +1,48 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_INFOBAR_H_
+#define CHROME_BROWSER_UI_COCOA_INFOBAR_H_
+#pragma once
+
+#include "base/logging.h" // for DCHECK
+
+@class InfoBarController;
+
+// A C++ wrapper around an Objective-C InfoBarController. This class
+// exists solely to be the return value for InfoBarDelegate::CreateInfoBar(),
+// as defined in chrome/browser/tab_contents/infobar_delegate.h. This
+// class would be analogous to the various bridge classes we already
+// have, but since there is no pre-defined InfoBar interface, it is
+// easier to simply throw away this object and deal with the
+// controller directly rather than pass messages through a bridge.
+//
+// Callers should delete the returned InfoBar immediately after
+// calling CreateInfoBar(), as the returned InfoBar* object is not
+// pointed to by anyone. Expected usage:
+//
+// scoped_ptr<InfoBar> infobar(delegate->CreateInfoBar());
+// InfoBarController* controller = infobar->controller();
+// // Do something with the controller, and save a pointer so it can be
+// // deleted later. |infobar| will be deleted automatically.
+
+class InfoBar {
+ public:
+ InfoBar(InfoBarController* controller) {
+ DCHECK(controller);
+ controller_ = controller;
+ }
+
+ InfoBarController* controller() {
+ return controller_;
+ }
+
+ private:
+ // Pointer to the infobar controller. Is never null.
+ InfoBarController* controller_; // weak
+
+ DISALLOW_COPY_AND_ASSIGN(InfoBar);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_INFOBAR_H_
diff --git a/chrome/browser/ui/cocoa/infobar_container_controller.h b/chrome/browser/ui/cocoa/infobar_container_controller.h
new file mode 100644
index 0000000..82a7e52
--- /dev/null
+++ b/chrome/browser/ui/cocoa/infobar_container_controller.h
@@ -0,0 +1,113 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_INFOBAR_CONTAINER_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_INFOBAR_CONTAINER_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#import "chrome/browser/ui/cocoa/view_resizer.h"
+#include "chrome/common/notification_registrar.h"
+
+@class InfoBarController;
+class InfoBarDelegate;
+class InfoBarNotificationObserver;
+class TabContents;
+class TabStripModel;
+
+// Protocol for basic container methods, as needed by an InfoBarController.
+// This protocol exists to make mocking easier in unittests.
+@protocol InfoBarContainer
+- (void)removeDelegate:(InfoBarDelegate*)delegate;
+- (void)removeController:(InfoBarController*)controller;
+@end
+
+// Controller for the infobar container view, which is the superview
+// of all the infobar views. This class owns zero or more
+// InfoBarControllers, which manage the infobar views. This class
+// also receives tab strip model notifications and handles
+// adding/removing infobars when needed.
+@interface InfoBarContainerController : NSViewController <ViewResizer,
+ InfoBarContainer> {
+ @private
+ // Needed to send resize messages when infobars are added or removed.
+ id<ViewResizer> resizeDelegate_; // weak
+
+ // The TabContents we are currently showing infobars for.
+ TabContents* currentTabContents_; // weak
+
+ // Holds the InfoBarControllers currently owned by this container.
+ scoped_nsobject<NSMutableArray> infobarControllers_;
+
+ // Lets us registers for INFOBAR_ADDED/INFOBAR_REMOVED
+ // notifications. The actual notifications are sent to the
+ // InfoBarNotificationObserver object, which proxies them back to us.
+ NotificationRegistrar registrar_;
+ scoped_ptr<InfoBarNotificationObserver> infoBarObserver_;
+}
+
+- (id)initWithResizeDelegate:(id<ViewResizer>)resizeDelegate;
+
+// Informs the selected TabContents that the infobars for the given
+// |delegate| need to be removed. Does not remove any infobar views
+// directly, as they will be removed when handling the subsequent
+// INFOBAR_REMOVED notification. Does not notify |delegate| that the
+// infobar was closed.
+- (void)removeDelegate:(InfoBarDelegate*)delegate;
+
+// Removes |controller| from the list of controllers in this container and
+// removes its view from the view hierarchy. This method is safe to call while
+// |controller| is still on the call stack.
+- (void)removeController:(InfoBarController*)controller;
+
+// Modifies this container to display infobars for the given
+// |contents|. Registers for INFOBAR_ADDED and INFOBAR_REMOVED
+// notifications for |contents|. If we are currently showing any
+// infobars, removes them first and deregisters for any
+// notifications. |contents| can be NULL, in which case no infobars
+// are shown and no notifications are registered for.
+- (void)changeTabContents:(TabContents*)contents;
+
+// Stripped down version of TabStripModelObserverBridge:tabDetachedWithContents.
+// Forwarded by BWC. Removes all infobars and deregisters for any notifications
+// if |contents| is the current tab contents.
+- (void)tabDetachedWithContents:(TabContents*)contents;
+
+@end
+
+
+@interface InfoBarContainerController (ForTheObserverAndTesting)
+
+// Adds an infobar view for the given delegate.
+- (void)addInfoBar:(InfoBarDelegate*)delegate animate:(BOOL)animate;
+
+// Closes all the infobar views for a given delegate, either immediately or by
+// starting a close animation.
+- (void)closeInfoBarsForDelegate:(InfoBarDelegate*)delegate
+ animate:(BOOL)animate;
+
+// Replaces all info bars for the delegate with a new info bar.
+// This simply calls closeInfoBarsForDelegate: and then addInfoBar:.
+- (void)replaceInfoBarsForDelegate:(InfoBarDelegate*)old_delegate
+ with:(InfoBarDelegate*)new_delegate;
+
+// Positions the infobar views in the container view and notifies
+// |browser_controller_| that it needs to resize the container view.
+- (void)positionInfoBarsAndRedraw;
+
+@end
+
+
+@interface InfoBarContainerController (JustForTesting)
+
+// Removes all infobar views. Callers must call
+// positionInfoBarsAndRedraw() after calling this method.
+- (void)removeAllInfoBars;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_INFOBAR_CONTAINER_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/infobar_container_controller.mm b/chrome/browser/ui/cocoa/infobar_container_controller.mm
new file mode 100644
index 0000000..b9d32cc
--- /dev/null
+++ b/chrome/browser/ui/cocoa/infobar_container_controller.mm
@@ -0,0 +1,228 @@
+// 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/logging.h"
+#include "base/mac_util.h"
+#include "chrome/browser/tab_contents/infobar_delegate.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#import "chrome/browser/ui/cocoa/animatable_view.h"
+#include "chrome/browser/ui/cocoa/infobar.h"
+#import "chrome/browser/ui/cocoa/infobar_container_controller.h"
+#import "chrome/browser/ui/cocoa/infobar_controller.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+#include "chrome/common/notification_service.h"
+#include "skia/ext/skia_utils_mac.h"
+
+// C++ class that receives INFOBAR_ADDED and INFOBAR_REMOVED
+// notifications and proxies them back to |controller|.
+class InfoBarNotificationObserver : public NotificationObserver {
+ public:
+ InfoBarNotificationObserver(InfoBarContainerController* controller)
+ : controller_(controller) {
+ }
+
+ private:
+ // NotificationObserver implementation
+ void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ switch (type.value) {
+ case NotificationType::TAB_CONTENTS_INFOBAR_ADDED:
+ [controller_ addInfoBar:Details<InfoBarDelegate>(details).ptr()
+ animate:YES];
+ break;
+ case NotificationType::TAB_CONTENTS_INFOBAR_REMOVED:
+ [controller_
+ closeInfoBarsForDelegate:Details<InfoBarDelegate>(details).ptr()
+ animate:YES];
+ break;
+ case NotificationType::TAB_CONTENTS_INFOBAR_REPLACED: {
+ typedef std::pair<InfoBarDelegate*, InfoBarDelegate*>
+ InfoBarDelegatePair;
+ InfoBarDelegatePair* delegates =
+ Details<InfoBarDelegatePair>(details).ptr();
+ [controller_
+ replaceInfoBarsForDelegate:delegates->first with:delegates->second];
+ break;
+ }
+ default:
+ NOTREACHED(); // we don't ask for anything else!
+ break;
+ }
+
+ [controller_ positionInfoBarsAndRedraw];
+ }
+
+ InfoBarContainerController* controller_; // weak, owns us.
+};
+
+
+@interface InfoBarContainerController (PrivateMethods)
+// Returns the desired height of the container view, computed by
+// adding together the heights of all its subviews.
+- (CGFloat)desiredHeight;
+
+@end
+
+
+@implementation InfoBarContainerController
+- (id)initWithResizeDelegate:(id<ViewResizer>)resizeDelegate {
+ DCHECK(resizeDelegate);
+ if ((self = [super initWithNibName:@"InfoBarContainer"
+ bundle:mac_util::MainAppBundle()])) {
+ resizeDelegate_ = resizeDelegate;
+ infoBarObserver_.reset(new InfoBarNotificationObserver(self));
+
+ // NSMutableArray needs an initial capacity, and we rarely ever see
+ // more than two infobars at a time, so that seems like a good choice.
+ infobarControllers_.reset([[NSMutableArray alloc] initWithCapacity:2]);
+ }
+ return self;
+}
+
+- (void)dealloc {
+ DCHECK([infobarControllers_ count] == 0);
+ view_id_util::UnsetID([self view]);
+ [super dealloc];
+}
+
+- (void)awakeFromNib {
+ // The info bar container view is an ordinary NSView object, so we set its
+ // ViewID here.
+ view_id_util::SetID([self view], VIEW_ID_INFO_BAR_CONTAINER);
+}
+
+- (void)removeDelegate:(InfoBarDelegate*)delegate {
+ DCHECK(currentTabContents_);
+ currentTabContents_->RemoveInfoBar(delegate);
+}
+
+- (void)removeController:(InfoBarController*)controller {
+ if (![infobarControllers_ containsObject:controller])
+ return;
+
+ // This code can be executed while InfoBarController is still on the stack, so
+ // we retain and autorelease the controller to prevent it from being
+ // dealloc'ed too early.
+ [[controller retain] autorelease];
+ [[controller view] removeFromSuperview];
+ [infobarControllers_ removeObject:controller];
+ [self positionInfoBarsAndRedraw];
+}
+
+- (void)changeTabContents:(TabContents*)contents {
+ registrar_.RemoveAll();
+ [self removeAllInfoBars];
+
+ currentTabContents_ = contents;
+ if (currentTabContents_) {
+ for (int i = 0; i < currentTabContents_->infobar_delegate_count(); ++i) {
+ [self addInfoBar:currentTabContents_->GetInfoBarDelegateAt(i)
+ animate:NO];
+ }
+
+ Source<TabContents> source(currentTabContents_);
+ registrar_.Add(infoBarObserver_.get(),
+ NotificationType::TAB_CONTENTS_INFOBAR_ADDED, source);
+ registrar_.Add(infoBarObserver_.get(),
+ NotificationType::TAB_CONTENTS_INFOBAR_REMOVED, source);
+ registrar_.Add(infoBarObserver_.get(),
+ NotificationType::TAB_CONTENTS_INFOBAR_REPLACED, source);
+ }
+
+ [self positionInfoBarsAndRedraw];
+}
+
+- (void)tabDetachedWithContents:(TabContents*)contents {
+ if (currentTabContents_ == contents)
+ [self changeTabContents:NULL];
+}
+
+- (void)resizeView:(NSView*)view newHeight:(CGFloat)height {
+ NSRect frame = [view frame];
+ frame.size.height = height;
+ [view setFrame:frame];
+ [self positionInfoBarsAndRedraw];
+}
+
+- (void)setAnimationInProgress:(BOOL)inProgress {
+ if ([resizeDelegate_ respondsToSelector:@selector(setAnimationInProgress:)])
+ [resizeDelegate_ setAnimationInProgress:inProgress];
+}
+
+@end
+
+@implementation InfoBarContainerController (PrivateMethods)
+
+- (CGFloat)desiredHeight {
+ CGFloat height = 0;
+ for (InfoBarController* controller in infobarControllers_.get())
+ height += NSHeight([[controller view] frame]);
+ return height;
+}
+
+- (void)addInfoBar:(InfoBarDelegate*)delegate animate:(BOOL)animate {
+ scoped_ptr<InfoBar> infobar(delegate->CreateInfoBar());
+ InfoBarController* controller = infobar->controller();
+ [controller setContainerController:self];
+ [[controller animatableView] setResizeDelegate:self];
+ [[self view] addSubview:[controller view]];
+ [infobarControllers_ addObject:[controller autorelease]];
+
+ if (animate)
+ [controller animateOpen];
+ else
+ [controller open];
+}
+
+- (void)closeInfoBarsForDelegate:(InfoBarDelegate*)delegate
+ animate:(BOOL)animate {
+ for (InfoBarController* controller in
+ [NSArray arrayWithArray:infobarControllers_.get()]) {
+ if ([controller delegate] == delegate) {
+ if (animate)
+ [controller animateClosed];
+ else
+ [controller close];
+ }
+ }
+}
+
+- (void)replaceInfoBarsForDelegate:(InfoBarDelegate*)old_delegate
+ with:(InfoBarDelegate*)new_delegate {
+ [self closeInfoBarsForDelegate:old_delegate animate:NO];
+ [self addInfoBar:new_delegate animate:NO];
+}
+
+- (void)removeAllInfoBars {
+ for (InfoBarController* controller in infobarControllers_.get()) {
+ [[controller animatableView] stopAnimation];
+ [[controller view] removeFromSuperview];
+ }
+ [infobarControllers_ removeAllObjects];
+}
+
+- (void)positionInfoBarsAndRedraw {
+ NSRect containerBounds = [[self view] bounds];
+ int minY = 0;
+
+ // Stack the infobars at the bottom of the view, starting with the
+ // last infobar and working our way to the front of the array. This
+ // way we ensure that the first infobar added shows up on top, with
+ // the others below.
+ for (InfoBarController* controller in
+ [infobarControllers_ reverseObjectEnumerator]) {
+ NSView* view = [controller view];
+ NSRect frame = [view frame];
+ frame.origin.x = NSMinX(containerBounds);
+ frame.size.width = NSWidth(containerBounds);
+ frame.origin.y = minY;
+ minY += frame.size.height;
+ [view setFrame:frame];
+ }
+
+ [resizeDelegate_ resizeView:[self view] newHeight:[self desiredHeight]];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/infobar_container_controller_unittest.mm b/chrome/browser/ui/cocoa/infobar_container_controller_unittest.mm
new file mode 100644
index 0000000..f04b1c1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/infobar_container_controller_unittest.mm
@@ -0,0 +1,95 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/infobar_container_controller.h"
+#include "chrome/browser/ui/cocoa/infobar_test_helper.h"
+#import "chrome/browser/ui/cocoa/view_resizer_pong.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class InfoBarContainerControllerTest : public CocoaTest {
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ resizeDelegate_.reset([[ViewResizerPong alloc] init]);
+ ViewResizerPong *viewResizer = resizeDelegate_.get();
+ controller_ =
+ [[InfoBarContainerController alloc] initWithResizeDelegate:viewResizer];
+ NSView* view = [controller_ view];
+ [[test_window() contentView] addSubview:view];
+ }
+
+ virtual void TearDown() {
+ [[controller_ view] removeFromSuperviewWithoutNeedingDisplay];
+ [controller_ release];
+ CocoaTest::TearDown();
+ }
+
+ public:
+ scoped_nsobject<ViewResizerPong> resizeDelegate_;
+ InfoBarContainerController* controller_;
+};
+
+TEST_VIEW(InfoBarContainerControllerTest, [controller_ view])
+
+TEST_F(InfoBarContainerControllerTest, BWCPong) {
+ // Call positionInfoBarsAndResize and check that |resizeDelegate_| got a
+ // resize message.
+ [resizeDelegate_ setHeight:-1];
+ [controller_ positionInfoBarsAndRedraw];
+ EXPECT_NE(-1, [resizeDelegate_ height]);
+}
+
+TEST_F(InfoBarContainerControllerTest, AddAndRemoveInfoBars) {
+ NSView* view = [controller_ view];
+
+ // Add three infobars, one of each type, and then remove them.
+ // After each step check to make sure we have the correct number of
+ // infobar subviews.
+ MockAlertInfoBarDelegate alertDelegate;
+ MockLinkInfoBarDelegate linkDelegate;
+ MockConfirmInfoBarDelegate confirmDelegate;
+
+ [controller_ addInfoBar:&alertDelegate animate:NO];
+ EXPECT_EQ(1U, [[view subviews] count]);
+
+ [controller_ addInfoBar:&linkDelegate animate:NO];
+ EXPECT_EQ(2U, [[view subviews] count]);
+
+ [controller_ addInfoBar:&confirmDelegate animate:NO];
+ EXPECT_EQ(3U, [[view subviews] count]);
+
+ // Just to mix things up, remove them in a different order.
+ [controller_ closeInfoBarsForDelegate:&linkDelegate animate:NO];
+ EXPECT_EQ(2U, [[view subviews] count]);
+
+ [controller_ closeInfoBarsForDelegate:&confirmDelegate animate:NO];
+ EXPECT_EQ(1U, [[view subviews] count]);
+
+ [controller_ closeInfoBarsForDelegate:&alertDelegate animate:NO];
+ EXPECT_EQ(0U, [[view subviews] count]);
+}
+
+TEST_F(InfoBarContainerControllerTest, RemoveAllInfoBars) {
+ NSView* view = [controller_ view];
+
+ // Add three infobars and then remove them all.
+ MockAlertInfoBarDelegate alertDelegate;
+ MockLinkInfoBarDelegate linkDelegate;
+ MockConfirmInfoBarDelegate confirmDelegate;
+
+ [controller_ addInfoBar:&alertDelegate animate:NO];
+ [controller_ addInfoBar:&linkDelegate animate:NO];
+ [controller_ addInfoBar:&confirmDelegate animate:NO];
+ EXPECT_EQ(3U, [[view subviews] count]);
+
+ [controller_ removeAllInfoBars];
+ EXPECT_EQ(0U, [[view subviews] count]);
+}
+} // namespace
diff --git a/chrome/browser/ui/cocoa/infobar_controller.h b/chrome/browser/ui/cocoa/infobar_controller.h
new file mode 100644
index 0000000..265700c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/infobar_controller.h
@@ -0,0 +1,106 @@
+// 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 "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+
+@class AnimatableView;
+@class HoverCloseButton;
+@protocol InfoBarContainer;
+class InfoBarDelegate;
+@class InfoBarGradientView;
+
+// A controller for an infobar in the browser window. There is one
+// controller per infobar view. The base InfoBarController is able to
+// draw an icon, a text message, and a close button. Subclasses can
+// override addAdditionalControls to customize the UI.
+@interface InfoBarController : NSViewController<NSTextViewDelegate> {
+ @private
+ id<InfoBarContainer> containerController_; // weak, owns us
+ BOOL infoBarClosing_;
+
+ @protected
+ IBOutlet InfoBarGradientView* infoBarView_;
+ IBOutlet NSImageView* image_;
+ IBOutlet NSTextField* labelPlaceholder_;
+ IBOutlet NSButton* okButton_;
+ IBOutlet NSButton* cancelButton_;
+ IBOutlet HoverCloseButton* closeButton_;
+
+ // In rare instances, it can be possible for |delegate_| to delete itself
+ // while this controller is still alive. Always check |delegate_| against
+ // NULL before using it.
+ InfoBarDelegate* delegate_; // weak, can be NULL
+
+ // Text fields don't work as well with embedded links as text views, but
+ // text views cannot conveniently be created in IB. The xib file contains
+ // a text field |labelPlaceholder_| that's replaced by this text view |label_|
+ // in -awakeFromNib.
+ scoped_nsobject<NSTextView> label_;
+};
+
+// Initializes a new InfoBarController.
+- (id)initWithDelegate:(InfoBarDelegate*)delegate;
+
+// Called when someone clicks on the OK or Cancel buttons. Subclasses
+// must override if they do not hide the buttons.
+- (void)ok:(id)sender;
+- (void)cancel:(id)sender;
+
+// Called when someone clicks on the close button. Dismisses the
+// infobar without taking any action.
+- (IBAction)dismiss:(id)sender;
+
+// Returns a pointer to this controller's view, cast as an AnimatableView.
+- (AnimatableView*)animatableView;
+
+// Open or animate open the infobar.
+- (void)open;
+- (void)animateOpen;
+
+// Close or animate close the infobar.
+- (void)close;
+- (void)animateClosed;
+
+// Subclasses can override this method to add additional controls to
+// the infobar view. This method is called by awakeFromNib. The
+// default implementation does nothing.
+- (void)addAdditionalControls;
+
+// Sets the info bar message to the specified |message|.
+- (void)setLabelToMessage:(NSString*)message;
+
+// Removes the OK and Cancel buttons and resizes the textfield to use the
+// space.
+- (void)removeButtons;
+
+@property(nonatomic, assign) id<InfoBarContainer> containerController;
+@property(nonatomic, readonly) InfoBarDelegate* delegate;
+
+@end
+
+/////////////////////////////////////////////////////////////////////////
+// InfoBarController subclasses, one for each InfoBarDelegate
+// subclass. Each of these subclasses overrides addAdditionalControls to
+// configure its view as necessary.
+
+@interface AlertInfoBarController : InfoBarController
+@end
+
+
+@interface LinkInfoBarController : InfoBarController
+// Called when there is a click on the link in the infobar.
+- (void)linkClicked;
+@end
+
+
+@interface ConfirmInfoBarController : InfoBarController
+// Called when the OK and Cancel buttons are clicked.
+- (IBAction)ok:(id)sender;
+- (IBAction)cancel:(id)sender;
+// Called when there is a click on the link in the infobar.
+- (void)linkClicked;
+@end
diff --git a/chrome/browser/ui/cocoa/infobar_controller.mm b/chrome/browser/ui/cocoa/infobar_controller.mm
new file mode 100644
index 0000000..1400c66
--- /dev/null
+++ b/chrome/browser/ui/cocoa/infobar_controller.mm
@@ -0,0 +1,534 @@
+// 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/logging.h" // for NOTREACHED()
+#include "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/tab_contents/infobar_delegate.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#import "chrome/browser/ui/cocoa/animatable_view.h"
+#include "chrome/browser/ui/cocoa/event_utils.h"
+#include "chrome/browser/ui/cocoa/infobar.h"
+#import "chrome/browser/ui/cocoa/infobar_container_controller.h"
+#import "chrome/browser/ui/cocoa/infobar_controller.h"
+#include "skia/ext/skia_utils_mac.h"
+#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+#include "webkit/glue/window_open_disposition.h"
+
+namespace {
+// Durations set to match the default SlideAnimation duration.
+const float kAnimateOpenDuration = 0.12;
+const float kAnimateCloseDuration = 0.12;
+}
+
+// This simple subclass of |NSTextView| just doesn't show the (text) cursor
+// (|NSTextView| displays the cursor with full keyboard accessibility enabled).
+@interface InfoBarTextView : NSTextView
+- (void)fixupCursor;
+@end
+
+@implementation InfoBarTextView
+
+// Never draw the insertion point (otherwise, it shows up without any user
+// action if full keyboard accessibility is enabled).
+- (BOOL)shouldDrawInsertionPoint {
+ return NO;
+}
+
+- (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange
+ granularity:(NSSelectionGranularity)granularity {
+ // Do not allow selections.
+ return NSMakeRange(0, 0);
+}
+
+// Convince NSTextView to not show an I-Beam cursor when the cursor is over the
+// text view but not over actual text.
+//
+// http://www.mail-archive.com/cocoa-dev@lists.apple.com/msg10791.html
+// "NSTextView sets the cursor over itself dynamically, based on considerations
+// including the text under the cursor. It does so in -mouseEntered:,
+// -mouseMoved:, and -cursorUpdate:, so those would be points to consider
+// overriding."
+- (void)mouseMoved:(NSEvent*)e {
+ [super mouseMoved:e];
+ [self fixupCursor];
+}
+
+- (void)mouseEntered:(NSEvent*)e {
+ [super mouseEntered:e];
+ [self fixupCursor];
+}
+
+- (void)cursorUpdate:(NSEvent*)e {
+ [super cursorUpdate:e];
+ [self fixupCursor];
+}
+
+- (void)fixupCursor {
+ if ([[NSCursor currentCursor] isEqual:[NSCursor IBeamCursor]])
+ [[NSCursor arrowCursor] set];
+}
+
+@end
+
+@interface InfoBarController (PrivateMethods)
+// Sets |label_| based on |labelPlaceholder_|, sets |labelPlaceholder_| to nil.
+- (void)initializeLabel;
+
+// Asks the container controller to remove the infobar for this delegate. This
+// call will trigger a notification that starts the infobar animating closed.
+- (void)removeInfoBar;
+
+// Performs final cleanup after an animation is finished or stopped, including
+// notifying the InfoBarDelegate that the infobar was closed and removing the
+// infobar from its container, if necessary.
+- (void)cleanUpAfterAnimation:(BOOL)finished;
+
+// Sets the info bar message to the specified |message|, with a hypertext
+// style link. |link| will be inserted into message at |linkOffset|.
+- (void)setLabelToMessage:(NSString*)message
+ withLink:(NSString*)link
+ atOffset:(NSUInteger)linkOffset;
+@end
+
+@implementation InfoBarController
+
+@synthesize containerController = containerController_;
+@synthesize delegate = delegate_;
+
+- (id)initWithDelegate:(InfoBarDelegate*)delegate {
+ DCHECK(delegate);
+ if ((self = [super initWithNibName:@"InfoBar"
+ bundle:mac_util::MainAppBundle()])) {
+ delegate_ = delegate;
+ }
+ return self;
+}
+
+// All infobars have an icon, so we set up the icon in the base class
+// awakeFromNib.
+- (void)awakeFromNib {
+ DCHECK(delegate_);
+ if (delegate_->GetIcon()) {
+ [image_ setImage:gfx::SkBitmapToNSImage(*(delegate_->GetIcon()))];
+ } else {
+ // No icon, remove it from the view and grow the textfield to include the
+ // space.
+ NSRect imageFrame = [image_ frame];
+ NSRect labelFrame = [labelPlaceholder_ frame];
+ labelFrame.size.width += NSMinX(imageFrame) - NSMinX(labelFrame);
+ labelFrame.origin.x = imageFrame.origin.x;
+ [image_ removeFromSuperview];
+ [labelPlaceholder_ setFrame:labelFrame];
+ }
+ [self initializeLabel];
+
+ [self addAdditionalControls];
+}
+
+// Called when someone clicks on the embedded link.
+- (BOOL) textView:(NSTextView*)textView
+ clickedOnLink:(id)link
+ atIndex:(NSUInteger)charIndex {
+ if ([self respondsToSelector:@selector(linkClicked)])
+ [self performSelector:@selector(linkClicked)];
+ return YES;
+}
+
+// Called when someone clicks on the ok button.
+- (void)ok:(id)sender {
+ // Subclasses must override this method if they do not hide the ok button.
+ NOTREACHED();
+}
+
+// Called when someone clicks on the cancel button.
+- (void)cancel:(id)sender {
+ // Subclasses must override this method if they do not hide the cancel button.
+ NOTREACHED();
+}
+
+// Called when someone clicks on the close button.
+- (void)dismiss:(id)sender {
+ [self removeInfoBar];
+}
+
+- (AnimatableView*)animatableView {
+ return static_cast<AnimatableView*>([self view]);
+}
+
+- (void)open {
+ // Simply reset the frame size to its opened size, forcing a relayout.
+ CGFloat finalHeight = [[self view] frame].size.height;
+ [[self animatableView] setHeight:finalHeight];
+}
+
+- (void)animateOpen {
+ // Force the frame size to be 0 and then start an animation.
+ NSRect frame = [[self view] frame];
+ CGFloat finalHeight = frame.size.height;
+ frame.size.height = 0;
+ [[self view] setFrame:frame];
+ [[self animatableView] animateToNewHeight:finalHeight
+ duration:kAnimateOpenDuration];
+}
+
+- (void)close {
+ // Stop any running animations.
+ [[self animatableView] stopAnimation];
+ infoBarClosing_ = YES;
+ [self cleanUpAfterAnimation:YES];
+}
+
+- (void)animateClosed {
+ // Start animating closed. We will receive a notification when the animation
+ // is done, at which point we can remove our view from the hierarchy and
+ // notify the delegate that the infobar was closed.
+ [[self animatableView] animateToNewHeight:0 duration:kAnimateCloseDuration];
+
+ // The above call may trigger an animationDidStop: notification for any
+ // currently-running animations, so do not set |infoBarClosing_| until after
+ // starting the animation.
+ infoBarClosing_ = YES;
+}
+
+- (void)addAdditionalControls {
+ // Default implementation does nothing.
+}
+
+- (void)setLabelToMessage:(NSString*)message {
+ NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
+ NSFont* font = [NSFont labelFontOfSize:
+ [NSFont systemFontSizeForControlSize:NSRegularControlSize]];
+ [attributes setObject:font
+ forKey:NSFontAttributeName];
+ [attributes setObject:[NSCursor arrowCursor]
+ forKey:NSCursorAttributeName];
+ scoped_nsobject<NSAttributedString> attributedString(
+ [[NSAttributedString alloc] initWithString:message
+ attributes:attributes]);
+ [[label_.get() textStorage] setAttributedString:attributedString];
+}
+
+- (void)removeButtons {
+ // Extend the label all the way across.
+ NSRect labelFrame = [label_.get() frame];
+ labelFrame.size.width = NSMaxX([cancelButton_ frame]) - NSMinX(labelFrame);
+ [okButton_ removeFromSuperview];
+ [cancelButton_ removeFromSuperview];
+ [label_.get() setFrame:labelFrame];
+}
+
+@end
+
+@implementation InfoBarController (PrivateMethods)
+
+- (void)initializeLabel {
+ // Replace the label placeholder NSTextField with the real label NSTextView.
+ // The former doesn't show links in a nice way, but the latter can't be added
+ // in IB without a containing scroll view, so create the NSTextView
+ // programmatically.
+ label_.reset([[InfoBarTextView alloc]
+ initWithFrame:[labelPlaceholder_ frame]]);
+ [label_.get() setAutoresizingMask:[labelPlaceholder_ autoresizingMask]];
+ [[labelPlaceholder_ superview]
+ replaceSubview:labelPlaceholder_ with:label_.get()];
+ labelPlaceholder_ = nil; // Now released.
+ [label_.get() setDelegate:self];
+ [label_.get() setEditable:NO];
+ [label_.get() setDrawsBackground:NO];
+ [label_.get() setHorizontallyResizable:NO];
+ [label_.get() setVerticallyResizable:NO];
+}
+
+- (void)removeInfoBar {
+ // TODO(rohitrao): This method can be called even if the infobar has already
+ // been removed and |delegate_| is NULL. Is there a way to rewrite the code
+ // so that inner event loops don't cause us to try and remove the infobar
+ // twice? http://crbug.com/54253
+ [containerController_ removeDelegate:delegate_];
+}
+
+- (void)cleanUpAfterAnimation:(BOOL)finished {
+ // Don't need to do any cleanup if the bar was animating open.
+ if (!infoBarClosing_)
+ return;
+
+ // Notify the delegate that the infobar was closed. The delegate may delete
+ // itself as a result of InfoBarClosed(), so we null out its pointer.
+ if (delegate_) {
+ delegate_->InfoBarClosed();
+ delegate_ = NULL;
+ }
+
+ // If the animation ran to completion, then we need to remove ourselves from
+ // the container. If the animation was interrupted, then the container will
+ // take care of removing us.
+ // TODO(rohitrao): UGH! This works for now, but should be cleaner.
+ if (finished)
+ [containerController_ removeController:self];
+}
+
+- (void)animationDidStop:(NSAnimation*)animation {
+ [self cleanUpAfterAnimation:NO];
+}
+
+- (void)animationDidEnd:(NSAnimation*)animation {
+ [self cleanUpAfterAnimation:YES];
+}
+
+// TODO(joth): This method factors out some common functionality between the
+// various derived infobar classes, however the class hierarchy itself could
+// use refactoring to reduce this duplication. http://crbug.com/38924
+- (void)setLabelToMessage:(NSString*)message
+ withLink:(NSString*)link
+ atOffset:(NSUInteger)linkOffset {
+ if (linkOffset == std::wstring::npos) {
+ // linkOffset == std::wstring::npos means the link should be right-aligned,
+ // which is not supported on Mac (http://crbug.com/47728).
+ NOTIMPLEMENTED();
+ linkOffset = [message length];
+ }
+ // Create an attributes dictionary for the entire message. We have
+ // to expicitly set the font the control's font. We also override
+ // the cursor to give us the normal cursor rather than the text
+ // insertion cursor.
+ NSMutableDictionary* linkAttributes = [NSMutableDictionary dictionary];
+ [linkAttributes setObject:[NSCursor arrowCursor]
+ forKey:NSCursorAttributeName];
+ NSFont* font = [NSFont labelFontOfSize:
+ [NSFont systemFontSizeForControlSize:NSRegularControlSize]];
+ [linkAttributes setObject:font
+ forKey:NSFontAttributeName];
+
+ // Create the attributed string for the main message text.
+ scoped_nsobject<NSMutableAttributedString> infoText(
+ [[NSMutableAttributedString alloc] initWithString:message]);
+ [infoText.get() addAttributes:linkAttributes
+ range:NSMakeRange(0, [infoText.get() length])];
+ // Add additional attributes to style the link text appropriately as
+ // well as linkify it.
+ [linkAttributes setObject:[NSColor blueColor]
+ forKey:NSForegroundColorAttributeName];
+ [linkAttributes setObject:[NSNumber numberWithBool:YES]
+ forKey:NSUnderlineStyleAttributeName];
+ [linkAttributes setObject:[NSCursor pointingHandCursor]
+ forKey:NSCursorAttributeName];
+ [linkAttributes setObject:[NSNumber numberWithInt:NSSingleUnderlineStyle]
+ forKey:NSUnderlineStyleAttributeName];
+ [linkAttributes setObject:[NSString string] // dummy value
+ forKey:NSLinkAttributeName];
+
+ // Insert the link text into the string at the appropriate offset.
+ scoped_nsobject<NSAttributedString> attributedString(
+ [[NSAttributedString alloc] initWithString:link
+ attributes:linkAttributes]);
+ [infoText.get() insertAttributedString:attributedString.get()
+ atIndex:linkOffset];
+ // Update the label view with the new text.
+ [[label_.get() textStorage] setAttributedString:infoText];
+}
+
+@end
+
+
+/////////////////////////////////////////////////////////////////////////
+// AlertInfoBarController implementation
+
+@implementation AlertInfoBarController
+
+// Alert infobars have a text message.
+- (void)addAdditionalControls {
+ // No buttons.
+ [self removeButtons];
+
+ // Insert the text.
+ AlertInfoBarDelegate* delegate = delegate_->AsAlertInfoBarDelegate();
+ DCHECK(delegate);
+ [self setLabelToMessage:base::SysUTF16ToNSString(delegate->GetMessageText())];
+}
+
+@end
+
+
+/////////////////////////////////////////////////////////////////////////
+// LinkInfoBarController implementation
+
+@implementation LinkInfoBarController
+
+// Link infobars have a text message, of which part is linkified. We
+// use an NSAttributedString to display styled text, and we set a
+// NSLink attribute on the hyperlink portion of the message. Infobars
+// use a custom NSTextField subclass, which allows us to override
+// textView:clickedOnLink:atIndex: and intercept clicks.
+//
+- (void)addAdditionalControls {
+ // No buttons.
+ [self removeButtons];
+
+ LinkInfoBarDelegate* delegate = delegate_->AsLinkInfoBarDelegate();
+ DCHECK(delegate);
+ size_t offset = std::wstring::npos;
+ string16 message = delegate->GetMessageTextWithOffset(&offset);
+ [self setLabelToMessage:base::SysUTF16ToNSString(message)
+ withLink:base::SysUTF16ToNSString(delegate->GetLinkText())
+ atOffset:offset];
+}
+
+// Called when someone clicks on the link in the infobar. This method
+// is called by the InfobarTextField on its delegate (the
+// LinkInfoBarController).
+- (void)linkClicked {
+ WindowOpenDisposition disposition =
+ event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
+ if (delegate_ && delegate_->AsLinkInfoBarDelegate()->LinkClicked(disposition))
+ [self removeInfoBar];
+}
+
+@end
+
+
+/////////////////////////////////////////////////////////////////////////
+// ConfirmInfoBarController implementation
+
+@implementation ConfirmInfoBarController
+
+// Called when someone clicks on the "OK" button.
+- (IBAction)ok:(id)sender {
+ if (delegate_ && delegate_->AsConfirmInfoBarDelegate()->Accept())
+ [self removeInfoBar];
+}
+
+// Called when someone clicks on the "Cancel" button.
+- (IBAction)cancel:(id)sender {
+ if (delegate_ && delegate_->AsConfirmInfoBarDelegate()->Cancel())
+ [self removeInfoBar];
+}
+
+// Confirm infobars can have OK and/or cancel buttons, depending on
+// the return value of GetButtons(). We create each button if
+// required and position them to the left of the close button.
+- (void)addAdditionalControls {
+ ConfirmInfoBarDelegate* delegate = delegate_->AsConfirmInfoBarDelegate();
+ DCHECK(delegate);
+ int visibleButtons = delegate->GetButtons();
+
+ NSRect okButtonFrame = [okButton_ frame];
+ NSRect cancelButtonFrame = [cancelButton_ frame];
+
+ DCHECK(NSMaxX(okButtonFrame) < NSMinX(cancelButtonFrame))
+ << "Cancel button expected to be on the right of the Ok button in nib";
+
+ CGFloat rightEdge = NSMaxX(cancelButtonFrame);
+ CGFloat spaceBetweenButtons =
+ NSMinX(cancelButtonFrame) - NSMaxX(okButtonFrame);
+ CGFloat spaceBeforeButtons =
+ NSMinX(okButtonFrame) - NSMaxX([label_.get() frame]);
+
+ // Update and position the Cancel button if needed. Otherwise, hide it.
+ if (visibleButtons & ConfirmInfoBarDelegate::BUTTON_CANCEL) {
+ [cancelButton_ setTitle:base::SysUTF16ToNSString(
+ delegate->GetButtonLabel(ConfirmInfoBarDelegate::BUTTON_CANCEL))];
+ [GTMUILocalizerAndLayoutTweaker sizeToFitView:cancelButton_];
+ cancelButtonFrame = [cancelButton_ frame];
+
+ // Position the cancel button to the left of the Close button.
+ cancelButtonFrame.origin.x = rightEdge - cancelButtonFrame.size.width;
+ [cancelButton_ setFrame:cancelButtonFrame];
+
+ // Update the rightEdge
+ rightEdge = NSMinX(cancelButtonFrame);
+ } else {
+ [cancelButton_ removeFromSuperview];
+ }
+
+ // Update and position the OK button if needed. Otherwise, hide it.
+ if (visibleButtons & ConfirmInfoBarDelegate::BUTTON_OK) {
+ [okButton_ setTitle:base::SysUTF16ToNSString(
+ delegate->GetButtonLabel(ConfirmInfoBarDelegate::BUTTON_OK))];
+ [GTMUILocalizerAndLayoutTweaker sizeToFitView:okButton_];
+ okButtonFrame = [okButton_ frame];
+
+ // If we had a Cancel button, leave space between the buttons.
+ if (visibleButtons & ConfirmInfoBarDelegate::BUTTON_CANCEL) {
+ rightEdge -= spaceBetweenButtons;
+ }
+
+ // Position the OK button on our current right edge.
+ okButtonFrame.origin.x = rightEdge - okButtonFrame.size.width;
+ [okButton_ setFrame:okButtonFrame];
+
+
+ // Update the rightEdge
+ rightEdge = NSMinX(okButtonFrame);
+ } else {
+ [okButton_ removeFromSuperview];
+ }
+
+ // If we had either button, leave space before the edge of the textfield.
+ if ((visibleButtons & ConfirmInfoBarDelegate::BUTTON_CANCEL) ||
+ (visibleButtons & ConfirmInfoBarDelegate::BUTTON_OK)) {
+ rightEdge -= spaceBeforeButtons;
+ }
+
+ NSRect frame = [label_.get() frame];
+ DCHECK(rightEdge > NSMinX(frame))
+ << "Need to make the xib larger to handle buttons with text this long";
+ frame.size.width = rightEdge - NSMinX(frame);
+ [label_.get() setFrame:frame];
+
+ // Set the text and link.
+ NSString* message = base::SysUTF16ToNSString(delegate->GetMessageText());
+ string16 link = delegate->GetLinkText();
+ if (link.empty()) {
+ // Simple case: no link, so just set the message directly.
+ [self setLabelToMessage:message];
+ } else {
+ // Inserting the link unintentionally causes the text to have a slightly
+ // different result to the simple case above: text is truncated on word
+ // boundaries (if needed) rather than elided with ellipses.
+
+ // Add spacing between the label and the link.
+ message = [message stringByAppendingString:@" "];
+ [self setLabelToMessage:message
+ withLink:base::SysUTF16ToNSString(link)
+ atOffset:[message length]];
+ }
+}
+
+// Called when someone clicks on the link in the infobar. This method
+// is called by the InfobarTextField on its delegate (the
+// LinkInfoBarController).
+- (void)linkClicked {
+ WindowOpenDisposition disposition =
+ event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
+ if (delegate_ &&
+ delegate_->AsConfirmInfoBarDelegate()->LinkClicked(disposition))
+ [self removeInfoBar];
+}
+
+@end
+
+
+//////////////////////////////////////////////////////////////////////////
+// CreateInfoBar() implementations
+
+InfoBar* AlertInfoBarDelegate::CreateInfoBar() {
+ AlertInfoBarController* controller =
+ [[AlertInfoBarController alloc] initWithDelegate:this];
+ return new InfoBar(controller);
+}
+
+InfoBar* LinkInfoBarDelegate::CreateInfoBar() {
+ LinkInfoBarController* controller =
+ [[LinkInfoBarController alloc] initWithDelegate:this];
+ return new InfoBar(controller);
+}
+
+InfoBar* ConfirmInfoBarDelegate::CreateInfoBar() {
+ ConfirmInfoBarController* controller =
+ [[ConfirmInfoBarController alloc] initWithDelegate:this];
+ return new InfoBar(controller);
+}
diff --git a/chrome/browser/ui/cocoa/infobar_controller_unittest.mm b/chrome/browser/ui/cocoa/infobar_controller_unittest.mm
new file mode 100644
index 0000000..4dfcd51
--- /dev/null
+++ b/chrome/browser/ui/cocoa/infobar_controller_unittest.mm
@@ -0,0 +1,284 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/tab_contents/infobar_delegate.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/infobar_container_controller.h"
+#import "chrome/browser/ui/cocoa/infobar_controller.h"
+#include "chrome/browser/ui/cocoa/infobar_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface InfoBarController (ExposedForTesting)
+- (NSString*)labelString;
+- (NSRect)labelFrame;
+@end
+
+@implementation InfoBarController (ExposedForTesting)
+- (NSString*)labelString {
+ return [label_.get() string];
+}
+- (NSRect)labelFrame {
+ return [label_.get() frame];
+}
+@end
+
+
+// Calls to removeDelegate: normally start an animation, which removes the
+// infobar completely when finished. For unittesting purposes, we create a mock
+// container which calls close: immediately, rather than kicking off an
+// animation.
+@interface InfoBarContainerTest : NSObject <InfoBarContainer> {
+ InfoBarController* controller_;
+}
+- (id)initWithController:(InfoBarController*)controller;
+- (void)removeDelegate:(InfoBarDelegate*)delegate;
+- (void)removeController:(InfoBarController*)controller;
+@end
+
+@implementation InfoBarContainerTest
+- (id)initWithController:(InfoBarController*)controller {
+ if ((self = [super init])) {
+ controller_ = controller;
+ }
+ return self;
+}
+
+- (void)removeDelegate:(InfoBarDelegate*)delegate {
+ [controller_ close];
+}
+
+- (void)removeController:(InfoBarController*)controller {
+ DCHECK(controller_ == controller);
+ controller_ = nil;
+}
+@end
+
+namespace {
+
+///////////////////////////////////////////////////////////////////////////
+// Test fixtures
+
+class AlertInfoBarControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+
+ controller_.reset(
+ [[AlertInfoBarController alloc] initWithDelegate:&delegate_]);
+ container_.reset(
+ [[InfoBarContainerTest alloc] initWithController:controller_]);
+ [controller_ setContainerController:container_];
+ [[test_window() contentView] addSubview:[controller_ view]];
+ }
+
+ protected:
+ MockAlertInfoBarDelegate delegate_;
+ scoped_nsobject<id> container_;
+ scoped_nsobject<AlertInfoBarController> controller_;
+};
+
+class LinkInfoBarControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+
+ controller_.reset(
+ [[LinkInfoBarController alloc] initWithDelegate:&delegate_]);
+ container_.reset(
+ [[InfoBarContainerTest alloc] initWithController:controller_]);
+ [controller_ setContainerController:container_];
+ [[test_window() contentView] addSubview:[controller_ view]];
+ }
+
+ protected:
+ MockLinkInfoBarDelegate delegate_;
+ scoped_nsobject<id> container_;
+ scoped_nsobject<LinkInfoBarController> controller_;
+};
+
+class ConfirmInfoBarControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+
+ controller_.reset(
+ [[ConfirmInfoBarController alloc] initWithDelegate:&delegate_]);
+ container_.reset(
+ [[InfoBarContainerTest alloc] initWithController:controller_]);
+ [controller_ setContainerController:container_];
+ [[test_window() contentView] addSubview:[controller_ view]];
+ }
+
+ protected:
+ MockConfirmInfoBarDelegate delegate_;
+ scoped_nsobject<id> container_;
+ scoped_nsobject<ConfirmInfoBarController> controller_;
+};
+
+
+////////////////////////////////////////////////////////////////////////////
+// Tests
+
+TEST_VIEW(AlertInfoBarControllerTest, [controller_ view]);
+
+TEST_F(AlertInfoBarControllerTest, ShowAndDismiss) {
+ // Make sure someone looked at the message and icon.
+ EXPECT_TRUE(delegate_.message_text_accessed);
+ EXPECT_TRUE(delegate_.icon_accessed);
+
+ // Check to make sure the infobar message was set properly.
+ EXPECT_EQ(kMockAlertInfoBarMessage,
+ base::SysNSStringToUTF8([controller_.get() labelString]));
+
+ // Check that dismissing the infobar calls InfoBarClosed() on the delegate.
+ [controller_ dismiss:nil];
+ EXPECT_TRUE(delegate_.closed);
+}
+
+TEST_F(AlertInfoBarControllerTest, DeallocController) {
+ // Test that dealloc'ing the controller does not send an
+ // InfoBarClosed() message to the delegate.
+ controller_.reset(nil);
+ EXPECT_FALSE(delegate_.closed);
+}
+
+TEST_F(AlertInfoBarControllerTest, ResizeView) {
+ NSRect originalLabelFrame = [controller_ labelFrame];
+
+ // Expand the view by 20 pixels and make sure the label frame changes
+ // accordingly.
+ const CGFloat width = 20;
+ NSRect newViewFrame = [[controller_ view] frame];
+ newViewFrame.size.width += width;
+ [[controller_ view] setFrame:newViewFrame];
+
+ NSRect newLabelFrame = [controller_ labelFrame];
+ EXPECT_EQ(NSWidth(newLabelFrame), NSWidth(originalLabelFrame) + width);
+}
+
+TEST_VIEW(LinkInfoBarControllerTest, [controller_ view]);
+
+TEST_F(LinkInfoBarControllerTest, ShowAndDismiss) {
+ // Make sure someone looked at the message, link, and icon.
+ EXPECT_TRUE(delegate_.message_text_accessed);
+ EXPECT_TRUE(delegate_.link_text_accessed);
+ EXPECT_TRUE(delegate_.icon_accessed);
+
+ // Check that dismissing the infobar calls InfoBarClosed() on the delegate.
+ [controller_ dismiss:nil];
+ EXPECT_FALSE(delegate_.link_clicked);
+ EXPECT_TRUE(delegate_.closed);
+}
+
+TEST_F(LinkInfoBarControllerTest, ShowAndClickLink) {
+ // Check that clicking on the link calls LinkClicked() on the
+ // delegate. It should also close the infobar.
+ [controller_ linkClicked];
+ EXPECT_TRUE(delegate_.link_clicked);
+ EXPECT_TRUE(delegate_.closed);
+}
+
+TEST_F(LinkInfoBarControllerTest, ShowAndClickLinkWithoutClosing) {
+ delegate_.closes_on_action = false;
+
+ // Check that clicking on the link calls LinkClicked() on the
+ // delegate. It should not close the infobar.
+ [controller_ linkClicked];
+ EXPECT_TRUE(delegate_.link_clicked);
+ EXPECT_FALSE(delegate_.closed);
+}
+
+TEST_VIEW(ConfirmInfoBarControllerTest, [controller_ view]);
+
+TEST_F(ConfirmInfoBarControllerTest, ShowAndDismiss) {
+ // Make sure someone looked at the message, link, and icon.
+ EXPECT_TRUE(delegate_.message_text_accessed);
+ EXPECT_TRUE(delegate_.link_text_accessed);
+ EXPECT_TRUE(delegate_.icon_accessed);
+
+ // Check to make sure the infobar message was set properly.
+ EXPECT_EQ(kMockConfirmInfoBarMessage,
+ base::SysNSStringToUTF8([controller_.get() labelString]));
+
+ // Check that dismissing the infobar calls InfoBarClosed() on the delegate.
+ [controller_ dismiss:nil];
+ EXPECT_FALSE(delegate_.ok_clicked);
+ EXPECT_FALSE(delegate_.cancel_clicked);
+ EXPECT_FALSE(delegate_.link_clicked);
+ EXPECT_TRUE(delegate_.closed);
+}
+
+TEST_F(ConfirmInfoBarControllerTest, ShowAndClickOK) {
+ // Check that clicking the OK button calls Accept() and then closes
+ // the infobar.
+ [controller_ ok:nil];
+ EXPECT_TRUE(delegate_.ok_clicked);
+ EXPECT_FALSE(delegate_.cancel_clicked);
+ EXPECT_FALSE(delegate_.link_clicked);
+ EXPECT_TRUE(delegate_.closed);
+}
+
+TEST_F(ConfirmInfoBarControllerTest, ShowAndClickOKWithoutClosing) {
+ delegate_.closes_on_action = false;
+
+ // Check that clicking the OK button calls Accept() but does not close
+ // the infobar.
+ [controller_ ok:nil];
+ EXPECT_TRUE(delegate_.ok_clicked);
+ EXPECT_FALSE(delegate_.cancel_clicked);
+ EXPECT_FALSE(delegate_.link_clicked);
+ EXPECT_FALSE(delegate_.closed);
+}
+
+TEST_F(ConfirmInfoBarControllerTest, ShowAndClickCancel) {
+ // Check that clicking the cancel button calls Cancel() and closes
+ // the infobar.
+ [controller_ cancel:nil];
+ EXPECT_FALSE(delegate_.ok_clicked);
+ EXPECT_TRUE(delegate_.cancel_clicked);
+ EXPECT_FALSE(delegate_.link_clicked);
+ EXPECT_TRUE(delegate_.closed);
+}
+
+TEST_F(ConfirmInfoBarControllerTest, ShowAndClickCancelWithoutClosing) {
+ delegate_.closes_on_action = false;
+
+ // Check that clicking the cancel button calls Cancel() but does not close
+ // the infobar.
+ [controller_ cancel:nil];
+ EXPECT_FALSE(delegate_.ok_clicked);
+ EXPECT_TRUE(delegate_.cancel_clicked);
+ EXPECT_FALSE(delegate_.link_clicked);
+ EXPECT_FALSE(delegate_.closed);
+}
+
+TEST_F(ConfirmInfoBarControllerTest, ShowAndClickLink) {
+ // Check that clicking on the link calls LinkClicked() on the
+ // delegate. It should also close the infobar.
+ [controller_ linkClicked];
+ EXPECT_FALSE(delegate_.ok_clicked);
+ EXPECT_FALSE(delegate_.cancel_clicked);
+ EXPECT_TRUE(delegate_.link_clicked);
+ EXPECT_TRUE(delegate_.closed);
+}
+
+TEST_F(ConfirmInfoBarControllerTest, ShowAndClickLinkWithoutClosing) {
+ delegate_.closes_on_action = false;
+
+ // Check that clicking on the link calls LinkClicked() on the
+ // delegate. It should not close the infobar.
+ [controller_ linkClicked];
+ EXPECT_FALSE(delegate_.ok_clicked);
+ EXPECT_FALSE(delegate_.cancel_clicked);
+ EXPECT_TRUE(delegate_.link_clicked);
+ EXPECT_FALSE(delegate_.closed);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/infobar_gradient_view.h b/chrome/browser/ui/cocoa/infobar_gradient_view.h
new file mode 100644
index 0000000..e0e0037
--- /dev/null
+++ b/chrome/browser/ui/cocoa/infobar_gradient_view.h
@@ -0,0 +1,19 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_INFOBAR_GRADIENT_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_INFOBAR_GRADIENT_VIEW_H_
+#pragma once
+
+#import "chrome/browser/ui/cocoa/vertical_gradient_view.h"
+
+#import <Cocoa/Cocoa.h>
+
+// A custom view that draws the background gradient for an infobar.
+@interface InfoBarGradientView : VerticalGradientView {
+}
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_INFOBAR_GRADIENT_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/infobar_gradient_view.mm b/chrome/browser/ui/cocoa/infobar_gradient_view.mm
new file mode 100644
index 0000000..cd2b1eeb
--- /dev/null
+++ b/chrome/browser/ui/cocoa/infobar_gradient_view.mm
@@ -0,0 +1,70 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/infobar_gradient_view.h"
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+
+namespace {
+
+const double kBackgroundColorTop[3] =
+ {255.0 / 255.0, 242.0 / 255.0, 183.0 / 255.0};
+const double kBackgroundColorBottom[3] =
+ {250.0 / 255.0, 230.0 / 255.0, 145.0 / 255.0};
+}
+
+@implementation InfoBarGradientView
+
+- (id)initWithFrame:(NSRect)frameRect {
+ if ((self = [super initWithFrame:frameRect])) {
+ NSColor* startingColor =
+ [NSColor colorWithCalibratedRed:kBackgroundColorTop[0]
+ green:kBackgroundColorTop[1]
+ blue:kBackgroundColorTop[2]
+ alpha:1.0];
+ NSColor* endingColor =
+ [NSColor colorWithCalibratedRed:kBackgroundColorBottom[0]
+ green:kBackgroundColorBottom[1]
+ blue:kBackgroundColorBottom[2]
+ alpha:1.0];
+ scoped_nsobject<NSGradient> gradient(
+ [[NSGradient alloc] initWithStartingColor:startingColor
+ endingColor:endingColor]);
+ [self setGradient:gradient];
+ }
+ return self;
+}
+
+- (NSColor*)strokeColor {
+ ThemeProvider* themeProvider = [[self window] themeProvider];
+ if (!themeProvider)
+ return [NSColor blackColor];
+
+ BOOL active = [[self window] isMainWindow];
+ return themeProvider->GetNSColor(
+ active ? BrowserThemeProvider::COLOR_TOOLBAR_STROKE :
+ BrowserThemeProvider::COLOR_TOOLBAR_STROKE_INACTIVE,
+ true);
+}
+
+- (BOOL)mouseDownCanMoveWindow {
+ return NO;
+}
+
+// This view is intentionally not opaque because it overlaps with the findbar.
+
+- (BOOL)accessibilityIsIgnored {
+ return NO;
+}
+
+- (id)accessibilityAttributeValue:(NSString*)attribute {
+ if ([attribute isEqual:NSAccessibilityRoleAttribute])
+ return NSAccessibilityGroupRole;
+
+ return [super accessibilityAttributeValue:attribute];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/infobar_gradient_view_unittest.mm b/chrome/browser/ui/cocoa/infobar_gradient_view_unittest.mm
new file mode 100644
index 0000000..2e4d01b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/infobar_gradient_view_unittest.mm
@@ -0,0 +1,32 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/infobar_gradient_view.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+
+namespace {
+
+class InfoBarGradientViewTest : public CocoaTest {
+ public:
+ InfoBarGradientViewTest() {
+ NSRect frame = NSMakeRect(0, 0, 100, 30);
+ scoped_nsobject<InfoBarGradientView> view(
+ [[InfoBarGradientView alloc] initWithFrame:frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ InfoBarGradientView* view_; // Weak. Retained by view hierarchy.
+};
+
+TEST_VIEW(InfoBarGradientViewTest, view_);
+
+// Assert that the view is non-opaque, because otherwise we will end
+// up with findbar painting issues.
+TEST_F(InfoBarGradientViewTest, AssertViewNonOpaque) {
+ EXPECT_FALSE([view_ isOpaque]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/infobar_test_helper.h b/chrome/browser/ui/cocoa/infobar_test_helper.h
new file mode 100644
index 0000000..d01a71b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/infobar_test_helper.h
@@ -0,0 +1,165 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/tab_contents/infobar_delegate.h"
+
+#include "base/utf_string_conversions.h"
+
+namespace {
+const char kMockAlertInfoBarMessage[] = "MockAlertInfoBarMessage";
+const char kMockLinkInfoBarMessage[] = "MockLinkInfoBarMessage";
+const char kMockLinkInfoBarLink[] = "http://dev.chromium.org";
+const char kMockConfirmInfoBarMessage[] = "MockConfirmInfoBarMessage";
+}
+
+//////////////////////////////////////////////////////////////////////////
+// Mock InfoBarDelgates
+
+class MockAlertInfoBarDelegate : public AlertInfoBarDelegate {
+ public:
+ explicit MockAlertInfoBarDelegate()
+ : AlertInfoBarDelegate(NULL),
+ message_text_accessed(false),
+ icon_accessed(false),
+ closed(false) {
+ }
+
+ virtual string16 GetMessageText() const {
+ message_text_accessed = true;
+ return ASCIIToUTF16(kMockAlertInfoBarMessage);
+ }
+
+ virtual SkBitmap* GetIcon() const {
+ icon_accessed = true;
+ return NULL;
+ }
+
+ virtual void InfoBarClosed() {
+ closed = true;
+ }
+
+ // These are declared mutable to get around const-ness issues.
+ mutable bool message_text_accessed;
+ mutable bool icon_accessed;
+ bool closed;
+};
+
+class MockLinkInfoBarDelegate : public LinkInfoBarDelegate {
+ public:
+ explicit MockLinkInfoBarDelegate()
+ : LinkInfoBarDelegate(NULL),
+ message_text_accessed(false),
+ link_text_accessed(false),
+ icon_accessed(false),
+ link_clicked(false),
+ closed(false),
+ closes_on_action(true) {
+ }
+
+ virtual string16 GetMessageTextWithOffset(size_t* link_offset) const {
+ message_text_accessed = true;
+ return ASCIIToUTF16(kMockLinkInfoBarMessage);
+ }
+
+ virtual string16 GetLinkText() const {
+ link_text_accessed = true;
+ return ASCIIToUTF16(kMockLinkInfoBarLink);
+ }
+
+ virtual SkBitmap* GetIcon() const {
+ icon_accessed = true;
+ return NULL;
+ }
+
+ virtual bool LinkClicked(WindowOpenDisposition disposition) {
+ link_clicked = true;
+ return closes_on_action;
+ }
+
+ virtual void InfoBarClosed() {
+ closed = true;
+ }
+
+ // These are declared mutable to get around const-ness issues.
+ mutable bool message_text_accessed;
+ mutable bool link_text_accessed;
+ mutable bool icon_accessed;
+ bool link_clicked;
+ bool closed;
+
+ // Determines whether the infobar closes when an action is taken or not.
+ bool closes_on_action;
+};
+
+class MockConfirmInfoBarDelegate : public ConfirmInfoBarDelegate {
+ public:
+ explicit MockConfirmInfoBarDelegate()
+ : ConfirmInfoBarDelegate(NULL),
+ message_text_accessed(false),
+ link_text_accessed(false),
+ icon_accessed(false),
+ ok_clicked(false),
+ cancel_clicked(false),
+ link_clicked(false),
+ closed(false),
+ closes_on_action(true) {
+ }
+
+ virtual int GetButtons() const {
+ return (BUTTON_OK | BUTTON_CANCEL);
+ }
+
+ virtual string16 GetButtonLabel(InfoBarButton button) const {
+ if (button == BUTTON_OK)
+ return ASCIIToUTF16("OK");
+ else
+ return ASCIIToUTF16("Cancel");
+ }
+
+ virtual bool Accept() {
+ ok_clicked = true;
+ return closes_on_action;
+ }
+
+ virtual bool Cancel() {
+ cancel_clicked = true;
+ return closes_on_action;
+ }
+
+ virtual string16 GetMessageText() const {
+ message_text_accessed = true;
+ return ASCIIToUTF16(kMockConfirmInfoBarMessage);
+ }
+
+ virtual SkBitmap* GetIcon() const {
+ icon_accessed = true;
+ return NULL;
+ }
+
+ virtual void InfoBarClosed() {
+ closed = true;
+ }
+
+ virtual string16 GetLinkText() {
+ link_text_accessed = true;
+ return string16();
+ }
+
+ virtual bool LinkClicked(WindowOpenDisposition disposition) {
+ link_clicked = true;
+ return closes_on_action;
+ }
+
+ // These are declared mutable to get around const-ness issues.
+ mutable bool message_text_accessed;
+ mutable bool link_text_accessed;
+ mutable bool icon_accessed;
+ bool ok_clicked;
+ bool cancel_clicked;
+ bool link_clicked;
+ bool closed;
+
+ // Determines whether the infobar closes when an action is taken or not.
+ bool closes_on_action;
+};
diff --git a/chrome/browser/ui/cocoa/install.sh b/chrome/browser/ui/cocoa/install.sh
new file mode 100755
index 0000000..dc73fae
--- /dev/null
+++ b/chrome/browser/ui/cocoa/install.sh
@@ -0,0 +1,123 @@
+#!/bin/bash -p
+
+# 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.
+
+# Called by the application to install in a new location. Generally, this
+# means that the application is running from a disk image and wants to be
+# copied to /Applications. The application, when running from the disk image,
+# will call this script to perform the copy.
+#
+# This script will be run as root if the application determines that it would
+# not otherwise have permission to perform the copy.
+#
+# When running as root, this script will be invoked with the real user ID set
+# to the user's ID, but the effective user ID set to 0 (root). bash -p is
+# used on the first line to prevent bash from setting the effective user ID to
+# the real user ID (dropping root privileges).
+
+set -e
+
+# This script may run as root, so be paranoid about things like ${PATH}.
+export PATH="/usr/bin:/usr/sbin:/bin:/sbin"
+
+# If running as root, output the pid to stdout before doing anything else.
+# See chrome/browser/cocoa/authorization_util.h.
+if [ ${EUID} -eq 0 ] ; then
+ echo "${$}"
+fi
+
+if [ ${#} -ne 2 ] ; then
+ echo "usage: ${0} SRC DEST" >& 2
+ exit 2
+fi
+
+SRC=${1}
+DEST=${2}
+
+# Make sure that SRC is an absolute path and that it exists.
+if [ -z "${SRC}" ] || [ "${SRC:0:1}" != "/" ] || [ ! -d "${SRC}" ] ; then
+ echo "${0}: source ${SRC} sanity check failed" >& 2
+ exit 3
+fi
+
+# Make sure that DEST is an absolute path and that it doesn't yet exist.
+if [ -z "${DEST}" ] || [ "${DEST:0:1}" != "/" ] || [ -e "${DEST}" ] ; then
+ echo "${0}: destination ${DEST} sanity check failed" >& 2
+ exit 4
+fi
+
+# Do the copy.
+rsync -lrpt "${SRC}/" "${DEST}"
+
+# The remaining steps are not considered critical.
+set +e
+
+# Notify LaunchServices.
+/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister "${DEST}"
+
+# If this script is not running as root and the application is installed
+# somewhere under /Applications, try to make it writable by all admin users.
+# This will allow other admin users to update the application from their own
+# user Keystone instances even if the Keystone ticket is not promoted to
+# system level.
+#
+# If the script is not running as root and the application is not installed
+# under /Applications, it might not be in a system-wide location, and it
+# probably won't be something that other users on the system are running, so
+# err on the side of safety and don't make it group-writable.
+#
+# If this script is running as root, a Keystone ticket promotion is expected,
+# and future updates can be expected to be applied as root, so
+# admin-writeability is not a concern. Set the entire thing to be owned by
+# root in that case, regardless of where it's installed, and drop any group
+# and other write permission.
+#
+# If this script is running as a user that is not a member of the admin group,
+# the chgrp operation will not succeed. Tolerate that case, because it's
+# better than the alternative, which is to make the application
+# world-writable.
+CHMOD_MODE="a+rX,u+w,go-w"
+if [ ${EUID} -ne 0 ] ; then
+ if [ "${DEST:0:14}" = "/Applications/" ] &&
+ chgrp -Rh admin "${DEST}" >& /dev/null ; then
+ CHMOD_MODE="a+rX,ug+w,o-w"
+ fi
+else
+ chown -Rh root:wheel "${DEST}" >& /dev/null
+fi
+
+chmod -R "${CHMOD_MODE}" "${DEST}" >& /dev/null
+
+# On the Mac, or at least on HFS+, symbolic link permissions are significant,
+# but chmod -R and -h can't be used together. Do another pass to fix the
+# permissions on any symbolic links.
+find "${DEST}" -type l -exec chmod -h "${CHMOD_MODE}" {} + >& /dev/null
+
+# Host OS version check, to be able to take advantage of features on newer
+# systems and fall back to slow ways of doing things on older systems.
+OS_VERSION=$(sw_vers -productVersion)
+OS_MAJOR=$(sed -Ene 's/^([0-9]+).*/\1/p' <<< ${OS_VERSION})
+OS_MINOR=$(sed -Ene 's/^([0-9]+)\.([0-9]+).*/\2/p' <<< ${OS_VERSION})
+
+# Because this script is launched by the application itself, the installation
+# process inherits the quarantine bit (LSFileQuarantineEnabled). Any files or
+# directories created during the update will be quarantined in that case,
+# which may cause Launch Services to display quarantine UI. That's bad,
+# especially if it happens when the outer .app launches a quarantined inner
+# helper. Since the user approved the application launch if quarantined, it
+# it can be assumed that the installed copy should not be quarantined. Use
+# xattr to drop the quarantine attribute.
+QUARANTINE_ATTR=com.apple.quarantine
+if [ ${OS_MAJOR} -gt 10 ] ||
+ ([ ${OS_MAJOR} -eq 10 ] && [ ${OS_MINOR} -ge 6 ]) ; then
+ # On 10.6, xattr supports -r for recursive operation.
+ xattr -d -r "${QUARANTINE_ATTR}" "${DEST}" >& /dev/null
+else
+ # On earlier systems, xattr doesn't support -r, so run xattr via find.
+ find "${DEST}" -exec xattr -d "${QUARANTINE_ATTR}" {} + >& /dev/null
+fi
+
+# Great success!
+exit 0
diff --git a/chrome/browser/ui/cocoa/install_from_dmg.h b/chrome/browser/ui/cocoa/install_from_dmg.h
new file mode 100644
index 0000000..0343cd0
--- /dev/null
+++ b/chrome/browser/ui/cocoa/install_from_dmg.h
@@ -0,0 +1,15 @@
+// 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_UI_COCOA_INSTALL_FROM_DMG_H_
+#define CHROME_BROWSER_UI_COCOA_INSTALL_FROM_DMG_H_
+#pragma once
+
+// If the application is running from a read-only disk image, prompts the user
+// to install it to the hard drive. If the user approves, the application
+// will be installed and launched, and MaybeInstallFromDiskImage will return
+// true. In that case, the caller must exit expeditiously.
+bool MaybeInstallFromDiskImage();
+
+#endif // CHROME_BROWSER_UI_COCOA_INSTALL_FROM_DMG_H_
diff --git a/chrome/browser/ui/cocoa/install_from_dmg.mm b/chrome/browser/ui/cocoa/install_from_dmg.mm
new file mode 100644
index 0000000..d18e257
--- /dev/null
+++ b/chrome/browser/ui/cocoa/install_from_dmg.mm
@@ -0,0 +1,438 @@
+// 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 "chrome/browser/ui/cocoa/install_from_dmg.h"
+
+#include <ApplicationServices/ApplicationServices.h>
+#import <AppKit/AppKit.h>
+#include <CoreFoundation/CoreFoundation.h>
+#include <CoreServices/CoreServices.h>
+#include <IOKit/IOKitLib.h>
+#include <string.h>
+#include <sys/param.h>
+#include <sys/mount.h>
+
+#include "app/l10n_util.h"
+#import "app/l10n_util_mac.h"
+#include "base/basictypes.h"
+#include "base/command_line.h"
+#include "base/logging.h"
+#import "base/mac_util.h"
+#include "base/mac/scoped_nsautorelease_pool.h"
+#include "chrome/browser/ui/cocoa/authorization_util.h"
+#include "chrome/browser/ui/cocoa/scoped_authorizationref.h"
+#import "chrome/browser/ui/cocoa/keystone_glue.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+
+// When C++ exceptions are disabled, the C++ library defines |try| and
+// |catch| so as to allow exception-expecting C++ code to build properly when
+// language support for exceptions is not present. These macros interfere
+// with the use of |@try| and |@catch| in Objective-C files such as this one.
+// Undefine these macros here, after everything has been #included, since
+// there will be no C++ uses and only Objective-C uses from this point on.
+#undef try
+#undef catch
+
+namespace {
+
+// Just like ScopedCFTypeRef but for io_object_t and subclasses.
+template<typename IOT>
+class scoped_ioobject {
+ public:
+ typedef IOT element_type;
+
+ explicit scoped_ioobject(IOT object = NULL)
+ : object_(object) {
+ }
+
+ ~scoped_ioobject() {
+ if (object_)
+ IOObjectRelease(object_);
+ }
+
+ void reset(IOT object = NULL) {
+ if (object_)
+ IOObjectRelease(object_);
+ object_ = object;
+ }
+
+ bool operator==(IOT that) const {
+ return object_ == that;
+ }
+
+ bool operator!=(IOT that) const {
+ return object_ != that;
+ }
+
+ operator IOT() const {
+ return object_;
+ }
+
+ IOT get() const {
+ return object_;
+ }
+
+ void swap(scoped_ioobject& that) {
+ IOT temp = that.object_;
+ that.object_ = object_;
+ object_ = temp;
+ }
+
+ IOT release() {
+ IOT temp = object_;
+ object_ = NULL;
+ return temp;
+ }
+
+ private:
+ IOT object_;
+
+ DISALLOW_COPY_AND_ASSIGN(scoped_ioobject);
+};
+
+// Returns true if |path| is located on a read-only filesystem of a disk
+// image. Returns false if not, or in the event of an error.
+bool IsPathOnReadOnlyDiskImage(const char path[]) {
+ struct statfs statfs_buf;
+ if (statfs(path, &statfs_buf) != 0) {
+ PLOG(ERROR) << "statfs " << path;
+ return false;
+ }
+
+ if (!(statfs_buf.f_flags & MNT_RDONLY)) {
+ // Not on a read-only filesystem.
+ return false;
+ }
+
+ const char dev_root[] = "/dev/";
+ const int dev_root_length = arraysize(dev_root) - 1;
+ if (strncmp(statfs_buf.f_mntfromname, dev_root, dev_root_length) != 0) {
+ // Not rooted at dev_root, no BSD name to search on.
+ return false;
+ }
+
+ // BSD names in IOKit don't include dev_root.
+ const char* bsd_device_name = statfs_buf.f_mntfromname + dev_root_length;
+
+ const mach_port_t master_port = kIOMasterPortDefault;
+
+ // IOBSDNameMatching gives ownership of match_dict to the caller, but
+ // IOServiceGetMatchingServices will assume that reference.
+ CFMutableDictionaryRef match_dict = IOBSDNameMatching(master_port,
+ 0,
+ bsd_device_name);
+ if (!match_dict) {
+ LOG(ERROR) << "IOBSDNameMatching " << bsd_device_name;
+ return false;
+ }
+
+ io_iterator_t iterator_ref;
+ kern_return_t kr = IOServiceGetMatchingServices(master_port,
+ match_dict,
+ &iterator_ref);
+ if (kr != KERN_SUCCESS) {
+ LOG(ERROR) << "IOServiceGetMatchingServices " << bsd_device_name
+ << ": kernel error " << kr;
+ return false;
+ }
+ scoped_ioobject<io_iterator_t> iterator(iterator_ref);
+ iterator_ref = NULL;
+
+ // There needs to be exactly one matching service.
+ scoped_ioobject<io_service_t> filesystem_service(IOIteratorNext(iterator));
+ if (!filesystem_service) {
+ LOG(ERROR) << "IOIteratorNext " << bsd_device_name << ": no service";
+ return false;
+ }
+ scoped_ioobject<io_service_t> unexpected_service(IOIteratorNext(iterator));
+ if (unexpected_service) {
+ LOG(ERROR) << "IOIteratorNext " << bsd_device_name << ": too many services";
+ return false;
+ }
+
+ iterator.reset();
+
+ const char disk_image_class[] = "IOHDIXController";
+
+ // This is highly unlikely. The filesystem service is expected to be of
+ // class IOMedia. Since the filesystem service's entire ancestor chain
+ // will be checked, though, check the filesystem service's class itself.
+ if (IOObjectConformsTo(filesystem_service, disk_image_class)) {
+ return true;
+ }
+
+ kr = IORegistryEntryCreateIterator(filesystem_service,
+ kIOServicePlane,
+ kIORegistryIterateRecursively |
+ kIORegistryIterateParents,
+ &iterator_ref);
+ if (kr != KERN_SUCCESS) {
+ LOG(ERROR) << "IORegistryEntryCreateIterator " << bsd_device_name
+ << ": kernel error " << kr;
+ return false;
+ }
+ iterator.reset(iterator_ref);
+ iterator_ref = NULL;
+
+ // Look at each of the filesystem service's ancestor services, beginning
+ // with the parent, iterating all the way up to the device tree's root. If
+ // any ancestor service matches the class used for disk images, the
+ // filesystem resides on a disk image.
+ for(scoped_ioobject<io_service_t> ancestor_service(IOIteratorNext(iterator));
+ ancestor_service;
+ ancestor_service.reset(IOIteratorNext(iterator))) {
+ if (IOObjectConformsTo(ancestor_service, disk_image_class)) {
+ return true;
+ }
+ }
+
+ // The filesystem does not reside on a disk image.
+ return false;
+}
+
+// Returns true if the application is located on a read-only filesystem of a
+// disk image. Returns false if not, or in the event of an error.
+bool IsAppRunningFromReadOnlyDiskImage() {
+ return IsPathOnReadOnlyDiskImage(
+ [[[NSBundle mainBundle] bundlePath] fileSystemRepresentation]);
+}
+
+// Shows a dialog asking the user whether or not to install from the disk
+// image. Returns true if the user approves installation.
+bool ShouldInstallDialog() {
+ NSString* title = l10n_util::GetNSStringFWithFixup(
+ IDS_INSTALL_FROM_DMG_TITLE, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
+ NSString* prompt = l10n_util::GetNSStringFWithFixup(
+ IDS_INSTALL_FROM_DMG_PROMPT, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
+ NSString* yes = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_YES);
+ NSString* no = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_NO);
+
+ NSAlert* alert = [[[NSAlert alloc] init] autorelease];
+
+ [alert setAlertStyle:NSInformationalAlertStyle];
+ [alert setMessageText:title];
+ [alert setInformativeText:prompt];
+ [alert addButtonWithTitle:yes];
+ NSButton* cancel_button = [alert addButtonWithTitle:no];
+ [cancel_button setKeyEquivalent:@"\e"];
+
+ NSInteger result = [alert runModal];
+
+ return result == NSAlertFirstButtonReturn;
+}
+
+// Potentially shows an authorization dialog to request authentication to
+// copy. If application_directory appears to be unwritable, attempts to
+// obtain authorization, which may result in the display of the dialog.
+// Returns NULL if authorization is not performed because it does not appear
+// to be necessary because the user has permission to write to
+// application_directory. Returns NULL if authorization fails.
+AuthorizationRef MaybeShowAuthorizationDialog(NSString* application_directory) {
+ NSFileManager* file_manager = [NSFileManager defaultManager];
+ if ([file_manager isWritableFileAtPath:application_directory]) {
+ return NULL;
+ }
+
+ NSString* prompt = l10n_util::GetNSStringFWithFixup(
+ IDS_INSTALL_FROM_DMG_AUTHENTICATION_PROMPT,
+ l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
+ return authorization_util::AuthorizationCreateToRunAsRoot(
+ reinterpret_cast<CFStringRef>(prompt));
+}
+
+// Invokes the installer program at installer_path to copy source_path to
+// target_path and perform any additional on-disk bookkeeping needed to be
+// able to launch target_path properly. If authorization_arg is non-NULL,
+// function will assume ownership of it, will invoke the installer with that
+// authorization reference, and will attempt Keystone ticket promotion.
+bool InstallFromDiskImage(AuthorizationRef authorization_arg,
+ NSString* installer_path,
+ NSString* source_path,
+ NSString* target_path) {
+ scoped_AuthorizationRef authorization(authorization_arg);
+ authorization_arg = NULL;
+ int exit_status;
+ if (authorization) {
+ const char* installer_path_c = [installer_path fileSystemRepresentation];
+ const char* source_path_c = [source_path fileSystemRepresentation];
+ const char* target_path_c = [target_path fileSystemRepresentation];
+ const char* arguments[] = {source_path_c, target_path_c, NULL};
+
+ OSStatus status = authorization_util::ExecuteWithPrivilegesAndWait(
+ authorization,
+ installer_path_c,
+ kAuthorizationFlagDefaults,
+ arguments,
+ NULL, // pipe
+ &exit_status);
+ if (status != errAuthorizationSuccess) {
+ LOG(ERROR) << "AuthorizationExecuteWithPrivileges install: " << status;
+ return false;
+ }
+ } else {
+ NSArray* arguments = [NSArray arrayWithObjects:source_path,
+ target_path,
+ nil];
+
+ NSTask* task;
+ @try {
+ task = [NSTask launchedTaskWithLaunchPath:installer_path
+ arguments:arguments];
+ } @catch(NSException* exception) {
+ LOG(ERROR) << "+[NSTask launchedTaskWithLaunchPath:arguments:]: "
+ << [[exception description] UTF8String];
+ return false;
+ }
+
+ [task waitUntilExit];
+ exit_status = [task terminationStatus];
+ }
+
+ if (exit_status != 0) {
+ LOG(ERROR) << "install.sh: exit status " << exit_status;
+ return false;
+ }
+
+ if (authorization) {
+ // As long as an AuthorizationRef is available, promote the Keystone
+ // ticket. Inform KeystoneGlue of the new path to use.
+ KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue];
+ [keystone_glue setAppPath:target_path];
+ [keystone_glue promoteTicketWithAuthorization:authorization.release()
+ synchronous:YES];
+ }
+
+ return true;
+}
+
+// Launches the application at app_path. The arguments passed to app_path
+// will be the same as the arguments used to invoke this process, except any
+// arguments beginning with -psn_ will be stripped.
+bool LaunchInstalledApp(NSString* app_path) {
+ const UInt8* app_path_c =
+ reinterpret_cast<const UInt8*>([app_path fileSystemRepresentation]);
+ FSRef app_fsref;
+ OSStatus err = FSPathMakeRef(app_path_c, &app_fsref, NULL);
+ if (err != noErr) {
+ LOG(ERROR) << "FSPathMakeRef: " << err;
+ return false;
+ }
+
+ const std::vector<std::string>& argv =
+ CommandLine::ForCurrentProcess()->argv();
+ NSMutableArray* arguments =
+ [NSMutableArray arrayWithCapacity:argv.size() - 1];
+ // Start at argv[1]. LSOpenApplication adds its own argv[0] as the path of
+ // the launched executable.
+ for (size_t index = 1; index < argv.size(); ++index) {
+ std::string argument = argv[index];
+ const char psn_flag[] = "-psn_";
+ const int psn_flag_length = arraysize(psn_flag) - 1;
+ if (argument.compare(0, psn_flag_length, psn_flag) != 0) {
+ // Strip any -psn_ arguments, as they apply to a specific process.
+ [arguments addObject:[NSString stringWithUTF8String:argument.c_str()]];
+ }
+ }
+
+ struct LSApplicationParameters parameters = {0};
+ parameters.flags = kLSLaunchDefaults;
+ parameters.application = &app_fsref;
+ parameters.argv = reinterpret_cast<CFArrayRef>(arguments);
+
+ err = LSOpenApplication(&parameters, NULL);
+ if (err != noErr) {
+ LOG(ERROR) << "LSOpenApplication: " << err;
+ return false;
+ }
+
+ return true;
+}
+
+void ShowErrorDialog() {
+ NSString* title = l10n_util::GetNSStringWithFixup(
+ IDS_INSTALL_FROM_DMG_ERROR_TITLE);
+ NSString* error = l10n_util::GetNSStringFWithFixup(
+ IDS_INSTALL_FROM_DMG_ERROR, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
+ NSString* ok = l10n_util::GetNSStringWithFixup(IDS_OK);
+
+ NSAlert* alert = [[[NSAlert alloc] init] autorelease];
+
+ [alert setAlertStyle:NSWarningAlertStyle];
+ [alert setMessageText:title];
+ [alert setInformativeText:error];
+ [alert addButtonWithTitle:ok];
+
+ [alert runModal];
+}
+
+} // namespace
+
+bool MaybeInstallFromDiskImage() {
+ base::mac::ScopedNSAutoreleasePool autorelease_pool;
+
+ if (!IsAppRunningFromReadOnlyDiskImage()) {
+ return false;
+ }
+
+ NSArray* application_directories =
+ NSSearchPathForDirectoriesInDomains(NSApplicationDirectory,
+ NSLocalDomainMask,
+ YES);
+ if ([application_directories count] == 0) {
+ LOG(ERROR) << "NSSearchPathForDirectoriesInDomains: "
+ << "no local application directories";
+ return false;
+ }
+ NSString* application_directory = [application_directories objectAtIndex:0];
+
+ NSFileManager* file_manager = [NSFileManager defaultManager];
+
+ BOOL is_directory;
+ if (![file_manager fileExistsAtPath:application_directory
+ isDirectory:&is_directory] ||
+ !is_directory) {
+ VLOG(1) << "No application directory at "
+ << [application_directory UTF8String];
+ return false;
+ }
+
+ NSString* source_path = [[NSBundle mainBundle] bundlePath];
+ NSString* application_name = [source_path lastPathComponent];
+ NSString* target_path =
+ [application_directory stringByAppendingPathComponent:application_name];
+
+ if ([file_manager fileExistsAtPath:target_path]) {
+ VLOG(1) << "Something already exists at " << [target_path UTF8String];
+ return false;
+ }
+
+ NSString* installer_path =
+ [mac_util::MainAppBundle() pathForResource:@"install" ofType:@"sh"];
+ if (!installer_path) {
+ VLOG(1) << "Could not locate install.sh";
+ return false;
+ }
+
+ if (!ShouldInstallDialog()) {
+ return false;
+ }
+
+ scoped_AuthorizationRef authorization(
+ MaybeShowAuthorizationDialog(application_directory));
+ // authorization will be NULL if it's deemed unnecessary or if
+ // authentication fails. In either case, try to install without privilege
+ // escalation.
+
+ if (!InstallFromDiskImage(authorization.release(),
+ installer_path,
+ source_path,
+ target_path) ||
+ !LaunchInstalledApp(target_path)) {
+ ShowErrorDialog();
+ return false;
+ }
+
+ return true;
+}
diff --git a/chrome/browser/ui/cocoa/instant_confirm_window_controller.h b/chrome/browser/ui/cocoa/instant_confirm_window_controller.h
new file mode 100644
index 0000000..223c197
--- /dev/null
+++ b/chrome/browser/ui/cocoa/instant_confirm_window_controller.h
@@ -0,0 +1,43 @@
+// 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_UI_COCOA_INSTANT_CONFIRM_WINDOW_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_INSTANT_CONFIRM_WINDOW_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/cocoa_protocols_mac.h"
+
+class Profile;
+
+// This controller manages a dialog that informs the user about Instant search.
+// The recommended API is to not use this class directly, but instead to use
+// the functions in //chrome/browser/instant/instant_confirm_dialog.h:
+// void ShowInstantConfirmDialog[IfNecessary](gfx::NativeWindow* parent, ...)
+// Which will attach the window to |parent| as a sheet.
+@interface InstantConfirmWindowController : NSWindowController<NSWindowDelegate>
+{
+ @private
+ // The long description about Instant that needs to be sized-to-fit.
+ IBOutlet NSTextField* description_;
+
+ Profile* profile_; // weak
+}
+
+// Designated initializer. The controller will release itself on window close.
+- (id)initWithProfile:(Profile*)profile;
+
+// Action for the "Learn more" link.
+- (IBAction)learnMore:(id)sender;
+
+// The user has opted in to Instant. This enables the Instant preference.
+- (IBAction)ok:(id)sender;
+
+// Closes the sheet without altering the preference value.
+- (IBAction)cancel:(id)sender;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_INSTANT_CONFIRM_WINDOW_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/instant_confirm_window_controller.mm b/chrome/browser/ui/cocoa/instant_confirm_window_controller.mm
new file mode 100644
index 0000000..a16f5e1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/instant_confirm_window_controller.mm
@@ -0,0 +1,76 @@
+// 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/ui/cocoa/instant_confirm_window_controller.h"
+
+#include "base/logging.h"
+#include "base/mac_util.h"
+#include "chrome/browser/instant/instant_confirm_dialog.h"
+#include "chrome/browser/instant/instant_controller.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/show_options_url.h"
+#include "gfx/native_widget_types.h"
+#include "googleurl/src/gurl.h"
+#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+namespace browser {
+
+void ShowInstantConfirmDialog(gfx::NativeWindow parent, Profile* profile) {
+ InstantConfirmWindowController* controller =
+ [[InstantConfirmWindowController alloc] initWithProfile:profile];
+ [NSApp beginSheet:[controller window]
+ modalForWindow:parent
+ modalDelegate:nil
+ didEndSelector:NULL
+ contextInfo:NULL];
+}
+
+} // namespace browser
+
+@implementation InstantConfirmWindowController
+
+- (id)initWithProfile:(Profile*)profile {
+ NSString* nibPath = [mac_util::MainAppBundle()
+ pathForResource:@"InstantConfirm"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
+ profile_ = profile;
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ DCHECK([self window]);
+ DCHECK_EQ(self, [[self window] delegate]);
+
+ CGFloat delta = [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:
+ description_];
+ NSRect descriptionFrame = [description_ frame];
+ descriptionFrame.origin.y -= delta;
+ [description_ setFrame:descriptionFrame];
+
+ NSRect frame = [[self window] frame];
+ frame.size.height += delta;
+ [[self window] setFrame:frame display:YES];
+}
+
+- (void)windowWillClose:(NSNotification*)notif {
+ [self autorelease];
+}
+
+- (IBAction)learnMore:(id)sender {
+ browser::ShowOptionsURL(profile_, GURL(browser::kInstantLearnMoreURL));
+}
+
+- (IBAction)ok:(id)sender {
+ InstantController::Enable(profile_);
+ [self cancel:sender];
+}
+
+- (IBAction)cancel:(id)sender {
+ [NSApp endSheet:[self window]];
+ [[self window] close];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/instant_confirm_window_controller_unittest.mm b/chrome/browser/ui/cocoa/instant_confirm_window_controller_unittest.mm
new file mode 100644
index 0000000..71d8c9e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/instant_confirm_window_controller_unittest.mm
@@ -0,0 +1,36 @@
+// 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/ui/cocoa/instant_confirm_window_controller.h"
+
+#include "chrome/browser/instant/instant_confirm_dialog.h"
+#import "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+
+namespace {
+
+class InstantConfirmWindowControllerTest : public CocoaTest {
+ public:
+ InstantConfirmWindowControllerTest() : controller_(nil) {}
+
+ BrowserTestHelper helper_;
+ InstantConfirmWindowController* controller_; // Weak. Owns self.
+};
+
+TEST_F(InstantConfirmWindowControllerTest, Init) {
+ controller_ =
+ [[InstantConfirmWindowController alloc] initWithProfile:
+ helper_.profile()];
+ EXPECT_TRUE([controller_ window]);
+ [controller_ release];
+}
+
+TEST_F(InstantConfirmWindowControllerTest, Show) {
+ browser::ShowInstantConfirmDialog(test_window(), helper_.profile());
+ controller_ = [[test_window() attachedSheet] windowController];
+ EXPECT_TRUE(controller_);
+ [controller_ cancel:nil];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/js_modal_dialog_cocoa.h b/chrome/browser/ui/cocoa/js_modal_dialog_cocoa.h
new file mode 100644
index 0000000..5005358
--- /dev/null
+++ b/chrome/browser/ui/cocoa/js_modal_dialog_cocoa.h
@@ -0,0 +1,48 @@
+// 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_UI_COCOA_JS_MODAL_DIALOG_COCOA_H_
+#define CHROME_BROWSER_UI_COCOA_JS_MODAL_DIALOG_COCOA_H_
+#pragma once
+
+#include "chrome/browser/native_app_modal_dialog.h"
+
+#include "base/logging.h"
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+
+#if __OBJC__
+@class NSAlert;
+@class JavaScriptAppModalDialogHelper;
+#else
+class NSAlert;
+class JavaScriptAppModalDialogHelper;
+#endif
+
+class JSModalDialogCocoa : public NativeAppModalDialog {
+ public:
+ explicit JSModalDialogCocoa(JavaScriptAppModalDialog* dialog);
+ virtual ~JSModalDialogCocoa();
+
+ // Overridden from NativeAppModalDialog:
+ virtual int GetAppModalDialogButtons() const;
+ virtual void ShowAppModalDialog();
+ virtual void ActivateAppModalDialog();
+ virtual void CloseAppModalDialog();
+ virtual void AcceptAppModalDialog();
+ virtual void CancelAppModalDialog();
+
+ JavaScriptAppModalDialog* dialog() const { return dialog_.get(); }
+
+ private:
+ scoped_ptr<JavaScriptAppModalDialog> dialog_;
+
+ scoped_nsobject<JavaScriptAppModalDialogHelper> helper_;
+ NSAlert* alert_; // weak, owned by |helper_|.
+
+ DISALLOW_COPY_AND_ASSIGN(JSModalDialogCocoa);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_JS_MODAL_DIALOG_COCOA_H_
+
diff --git a/chrome/browser/ui/cocoa/js_modal_dialog_cocoa.mm b/chrome/browser/ui/cocoa/js_modal_dialog_cocoa.mm
new file mode 100644
index 0000000..f604ce3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/js_modal_dialog_cocoa.mm
@@ -0,0 +1,219 @@
+// 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 "chrome/browser/ui/cocoa/js_modal_dialog_cocoa.h"
+
+#import <Cocoa/Cocoa.h>
+
+#include "app/l10n_util_mac.h"
+#include "app/message_box_flags.h"
+#import "base/cocoa_protocols_mac.h"
+#include "base/logging.h"
+#include "base/sys_string_conversions.h"
+#import "chrome/browser/chrome_browser_application_mac.h"
+#include "chrome/browser/js_modal_dialog.h"
+#include "grit/app_strings.h"
+#include "grit/generated_resources.h"
+
+// Helper object that receives the notification that the dialog/sheet is
+// going away. Is responsible for cleaning itself up.
+@interface JavaScriptAppModalDialogHelper : NSObject<NSAlertDelegate> {
+ @private
+ NSAlert* alert_;
+ NSTextField* textField_; // WEAK; owned by alert_
+}
+
+- (NSAlert*)alert;
+- (NSTextField*)textField;
+- (void)alertDidEnd:(NSAlert *)alert
+ returnCode:(int)returnCode
+ contextInfo:(void*)contextInfo;
+
+@end
+
+@implementation JavaScriptAppModalDialogHelper
+
+- (NSAlert*)alert {
+ alert_ = [[NSAlert alloc] init];
+ return alert_;
+}
+
+- (NSTextField*)textField {
+ textField_ = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 300, 22)];
+ [alert_ setAccessoryView:textField_];
+ [textField_ release];
+
+ return textField_;
+}
+
+- (void)dealloc {
+ [alert_ release];
+ [super dealloc];
+}
+
+// |contextInfo| is the JSModalDialogCocoa that owns us.
+- (void)alertDidEnd:(NSAlert*)alert
+ returnCode:(int)returnCode
+ contextInfo:(void*)contextInfo {
+ scoped_ptr<JSModalDialogCocoa> native_dialog(
+ reinterpret_cast<JSModalDialogCocoa*>(contextInfo));
+ std::wstring input;
+ if (textField_)
+ input = base::SysNSStringToWide([textField_ stringValue]);
+ bool shouldSuppress = false;
+ if ([alert showsSuppressionButton])
+ shouldSuppress = [[alert suppressionButton] state] == NSOnState;
+ switch (returnCode) {
+ case NSAlertFirstButtonReturn: { // OK
+ native_dialog->dialog()->OnAccept(input, shouldSuppress);
+ break;
+ }
+ case NSAlertSecondButtonReturn: { // Cancel
+ // If the user wants to stay on this page, stop quitting (if a quit is in
+ // progress).
+ if (native_dialog->dialog()->is_before_unload_dialog())
+ chrome_browser_application_mac::CancelTerminate();
+ native_dialog->dialog()->OnCancel(shouldSuppress);
+ break;
+ }
+ case NSRunStoppedResponse: { // Window was closed underneath us
+ // Need to call OnCancel() because there is some cleanup that needs
+ // to be done. It won't call back to the javascript since the
+ // JavaScriptAppModalDialog knows that the TabContents was destroyed.
+ native_dialog->dialog()->OnCancel(shouldSuppress);
+ break;
+ }
+ default: {
+ NOTREACHED();
+ }
+ }
+}
+@end
+
+////////////////////////////////////////////////////////////////////////////////
+// JSModalDialogCocoa, public:
+
+JSModalDialogCocoa::JSModalDialogCocoa(JavaScriptAppModalDialog* dialog)
+ : dialog_(dialog),
+ helper_(NULL) {
+ // Determine the names of the dialog buttons based on the flags. "Default"
+ // is the OK button. "Other" is the cancel button. We don't use the
+ // "Alternate" button in NSRunAlertPanel.
+ NSString* default_button = l10n_util::GetNSStringWithFixup(IDS_APP_OK);
+ NSString* other_button = l10n_util::GetNSStringWithFixup(IDS_APP_CANCEL);
+ bool text_field = false;
+ bool one_button = false;
+ switch (dialog_->dialog_flags()) {
+ case MessageBoxFlags::kIsJavascriptAlert:
+ one_button = true;
+ break;
+ case MessageBoxFlags::kIsJavascriptConfirm:
+ if (dialog_->is_before_unload_dialog()) {
+ default_button = l10n_util::GetNSStringWithFixup(
+ IDS_BEFOREUNLOAD_MESSAGEBOX_OK_BUTTON_LABEL);
+ other_button = l10n_util::GetNSStringWithFixup(
+ IDS_BEFOREUNLOAD_MESSAGEBOX_CANCEL_BUTTON_LABEL);
+ }
+ break;
+ case MessageBoxFlags::kIsJavascriptPrompt:
+ text_field = true;
+ break;
+
+ default:
+ NOTREACHED();
+ }
+
+ // Create a helper which will receive the sheet ended selector. It will
+ // delete itself when done. It doesn't need anything passed to its init
+ // as it will get a contextInfo parameter.
+ helper_.reset([[JavaScriptAppModalDialogHelper alloc] init]);
+
+ // Show the modal dialog.
+ alert_ = [helper_ alert];
+ NSTextField* field = nil;
+ if (text_field) {
+ field = [helper_ textField];
+ [field setStringValue:base::SysWideToNSString(
+ dialog_->default_prompt_text())];
+ }
+ [alert_ setDelegate:helper_];
+ [alert_ setInformativeText:base::SysWideToNSString(dialog_->message_text())];
+ [alert_ setMessageText:base::SysWideToNSString(dialog_->title())];
+ [alert_ addButtonWithTitle:default_button];
+ if (!one_button) {
+ NSButton* other = [alert_ addButtonWithTitle:other_button];
+ [other setKeyEquivalent:@"\e"];
+ }
+ if (dialog_->display_suppress_checkbox()) {
+ [alert_ setShowsSuppressionButton:YES];
+ NSString* suppression_title = l10n_util::GetNSStringWithFixup(
+ IDS_JAVASCRIPT_MESSAGEBOX_SUPPRESS_OPTION);
+ [[alert_ suppressionButton] setTitle:suppression_title];
+ }
+}
+
+JSModalDialogCocoa::~JSModalDialogCocoa() {
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// JSModalDialogCocoa, NativeAppModalDialog implementation:
+
+int JSModalDialogCocoa::GetAppModalDialogButtons() const {
+ // From the above, it is the case that if there is 1 button, it is always the
+ // OK button. The second button, if it exists, is always the Cancel button.
+ int num_buttons = [[alert_ buttons] count];
+ switch (num_buttons) {
+ case 1:
+ return MessageBoxFlags::DIALOGBUTTON_OK;
+ case 2:
+ return MessageBoxFlags::DIALOGBUTTON_OK |
+ MessageBoxFlags::DIALOGBUTTON_CANCEL;
+ default:
+ NOTREACHED();
+ return 0;
+ }
+}
+
+void JSModalDialogCocoa::ShowAppModalDialog() {
+ [alert_
+ beginSheetModalForWindow:nil // nil here makes it app-modal
+ modalDelegate:helper_.get()
+ didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
+ contextInfo:this];
+
+ if ([alert_ accessoryView])
+ [[alert_ window] makeFirstResponder:[alert_ accessoryView]];
+}
+
+void JSModalDialogCocoa::ActivateAppModalDialog() {
+}
+
+void JSModalDialogCocoa::CloseAppModalDialog() {
+ DCHECK([alert_ isKindOfClass:[NSAlert class]]);
+
+ // Note: the call below will delete |this|,
+ // see JavaScriptAppModalDialogHelper's alertDidEnd.
+ [NSApp endSheet:[alert_ window]];
+}
+
+void JSModalDialogCocoa::AcceptAppModalDialog() {
+ NSButton* first = [[alert_ buttons] objectAtIndex:0];
+ [first performClick:nil];
+}
+
+void JSModalDialogCocoa::CancelAppModalDialog() {
+ DCHECK([[alert_ buttons] count] >= 2);
+ NSButton* second = [[alert_ buttons] objectAtIndex:1];
+ [second performClick:nil];
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// NativeAppModalDialog, public:
+
+// static
+NativeAppModalDialog* NativeAppModalDialog::CreateNativeJavaScriptPrompt(
+ JavaScriptAppModalDialog* dialog,
+ gfx::NativeWindow parent_window) {
+ return new JSModalDialogCocoa(dialog);
+}
diff --git a/chrome/browser/ui/cocoa/keystone_glue.h b/chrome/browser/ui/cocoa/keystone_glue.h
new file mode 100644
index 0000000..c20bdb4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/keystone_glue.h
@@ -0,0 +1,209 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_KEYSTONE_GLUE_H_
+#define CHROME_BROWSER_UI_COCOA_KEYSTONE_GLUE_H_
+#pragma once
+
+#include "base/string16.h"
+
+#if defined(__OBJC__)
+
+#import <Foundation/Foundation.h>
+
+#import "base/scoped_nsobject.h"
+#include "chrome/browser/ui/cocoa/scoped_authorizationref.h"
+
+// Possible outcomes of various operations. A version may accompany some of
+// these, but beware: a version is never required. For statuses that can be
+// accompanied by a version, the comment indicates what version is referenced.
+// A notification posted containing an asynchronous status will always be
+// followed by a notification with a terminal status.
+enum AutoupdateStatus {
+ kAutoupdateNone = 0, // no version (initial state only)
+ kAutoupdateRegistering, // no version (asynchronous operation in progress)
+ kAutoupdateRegistered, // no version
+ kAutoupdateChecking, // no version (asynchronous operation in progress)
+ kAutoupdateCurrent, // version of the running application
+ kAutoupdateAvailable, // version of the update that is available
+ kAutoupdateInstalling, // no version (asynchronous operation in progress)
+ kAutoupdateInstalled, // version of the update that was installed
+ kAutoupdatePromoting, // no version (asynchronous operation in progress)
+ kAutoupdatePromoted, // no version
+ kAutoupdateRegisterFailed, // no version
+ kAutoupdateCheckFailed, // no version
+ kAutoupdateInstallFailed, // no version
+ kAutoupdatePromoteFailed, // no version
+};
+
+// kAutoupdateStatusNotification is the name of the notification posted when
+// -checkForUpdate and -installUpdate complete. This notification will be
+// sent with with its sender object set to the KeystoneGlue instance sending
+// the notification. Its userInfo dictionary will contain an AutoupdateStatus
+// value as an intValue at key kAutoupdateStatusStatus. If a version is
+// available (see AutoupdateStatus), it will be present at key
+// kAutoupdateStatusVersion.
+extern NSString* const kAutoupdateStatusNotification;
+extern NSString* const kAutoupdateStatusStatus;
+extern NSString* const kAutoupdateStatusVersion;
+
+namespace {
+
+enum BrandFileType {
+ kBrandFileTypeNotDetermined = 0,
+ kBrandFileTypeNone,
+ kBrandFileTypeUser,
+ kBrandFileTypeSystem,
+};
+
+} // namespace
+
+// KeystoneGlue is an adapter around the KSRegistration class, allowing it to
+// be used without linking directly against its containing KeystoneRegistration
+// framework. This is used in an environment where most builds (such as
+// developer builds) don't want or need Keystone support and might not even
+// have the framework available. Enabling Keystone support in an application
+// that uses KeystoneGlue is as simple as dropping
+// KeystoneRegistration.framework in the application's Frameworks directory
+// and providing the relevant information in its Info.plist. KeystoneGlue
+// requires that the KSUpdateURL key be set in the application's Info.plist,
+// and that it contain a string identifying the update URL to be used by
+// Keystone.
+
+@class KSRegistration;
+
+@interface KeystoneGlue : NSObject {
+ @protected
+
+ // Data for Keystone registration
+ NSString* productID_;
+ NSString* appPath_;
+ NSString* url_;
+ NSString* version_;
+ NSString* channel_; // Logically: Dev, Beta, or Stable.
+ BrandFileType brandFileType_;
+
+ // And the Keystone registration itself, with the active timer
+ KSRegistration* registration_; // strong
+ NSTimer* timer_; // strong
+
+ // The most recent kAutoupdateStatusNotification notification posted.
+ scoped_nsobject<NSNotification> recentNotification_;
+
+ // The authorization object, when it needs to persist because it's being
+ // carried across threads.
+ scoped_AuthorizationRef authorization_;
+
+ // YES if a synchronous promotion operation is in progress (promotion during
+ // installation).
+ BOOL synchronousPromotion_;
+
+ // YES if an update was ever successfully installed by -installUpdate.
+ BOOL updateSuccessfullyInstalled_;
+}
+
+// Return the default Keystone Glue object.
++ (id)defaultKeystoneGlue;
+
+// Load KeystoneRegistration.framework if present, call into it to register
+// with Keystone, and set up periodic activity pings.
+- (void)registerWithKeystone;
+
+// -checkForUpdate launches a check for updates, and -installUpdate begins
+// installing an available update. For each, status will be communicated via
+// a kAutoupdateStatusNotification notification, and will also be available
+// through -recentNotification.
+- (void)checkForUpdate;
+- (void)installUpdate;
+
+// Accessor for recentNotification_. Returns an autoreleased NSNotification.
+- (NSNotification*)recentNotification;
+
+// Accessor for the kAutoupdateStatusStatus field of recentNotification_'s
+// userInfo dictionary.
+- (AutoupdateStatus)recentStatus;
+
+// Returns YES if an asynchronous operation is pending: if an update check or
+// installation attempt is currently in progress.
+- (BOOL)asyncOperationPending;
+
+// Returns YES if the application is running from a read-only filesystem,
+// such as a disk image.
+- (BOOL)isOnReadOnlyFilesystem;
+
+// -needsPromotion is YES if the application needs its ticket promoted to
+// a system ticket. This will be YES when the application is on a user
+// ticket and determines that the current user does not have sufficient
+// permission to perform the update.
+//
+// -wantsPromotion is YES if the application wants its ticket promoted to
+// a system ticket, even if it doesn't need it as determined by
+// -needsPromotion. -wantsPromotion will always be YES if -needsPromotion is,
+// and it will additionally be YES when the application is on a user ticket
+// and appears to be installed in a system-wide location such as
+// /Applications.
+//
+// Use -needsPromotion to decide whether to show any update UI at all. If
+// it's YES, there's no sense in asking the user to "update now" because it
+// will fail given the rights and permissions involved. On the other hand,
+// when -needsPromotion is YES, the application can encourage the user to
+// promote the ticket so that updates will work properly.
+//
+// Use -wantsPromotion to decide whether to allow the user to promote. The
+// user shouldn't be nagged about promotion on the basis of -wantsPromotion,
+// but if it's YES, the user should be allowed to promote the ticket.
+- (BOOL)needsPromotion;
+- (BOOL)wantsPromotion;
+
+// Promotes the Keystone ticket into the system store. System Keystone will
+// be installed if necessary. If synchronous is NO, the promotion may occur
+// in the background. synchronous should be YES for promotion during
+// installation. The KeystoneGlue object assumes ownership of
+// authorization_arg.
+- (void)promoteTicketWithAuthorization:(AuthorizationRef)authorization_arg
+ synchronous:(BOOL)synchronous;
+
+// Requests authorization and calls -promoteTicketWithAuthorization: in
+// asynchronous mode.
+- (void)promoteTicket;
+
+// Sets a new value for appPath. Used during installation to point a ticket
+// at the installed copy.
+- (void)setAppPath:(NSString*)appPath;
+
+@end // @interface KeystoneGlue
+
+@interface KeystoneGlue(ExposedForTesting)
+
+// Load any params we need for configuring Keystone.
+- (void)loadParameters;
+
+// Load the Keystone registration object.
+// Return NO on failure.
+- (BOOL)loadKeystoneRegistration;
+
+- (void)stopTimer;
+
+// Called when a checkForUpdate: notification completes.
+- (void)checkForUpdateComplete:(NSNotification*)notification;
+
+// Called when an installUpdate: notification completes.
+- (void)installUpdateComplete:(NSNotification*)notification;
+
+@end // @interface KeystoneGlue(ExposedForTesting)
+
+#endif // __OBJC__
+
+// Functions that may be accessed from non-Objective-C C/C++ code.
+namespace keystone_glue {
+
+// True if Keystone is enabled.
+bool KeystoneEnabled();
+
+// The version of the application currently installed on disk.
+string16 CurrentlyInstalledVersion();
+
+} // namespace keystone_glue
+
+#endif // CHROME_BROWSER_UI_COCOA_KEYSTONE_GLUE_H_
diff --git a/chrome/browser/ui/cocoa/keystone_glue.mm b/chrome/browser/ui/cocoa/keystone_glue.mm
new file mode 100644
index 0000000..fa9924d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/keystone_glue.mm
@@ -0,0 +1,959 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/keystone_glue.h"
+
+#include <sys/param.h>
+#include <sys/mount.h>
+
+#include <vector>
+
+#include "app/l10n_util.h"
+#import "app/l10n_util_mac.h"
+#include "base/logging.h"
+#include "base/mac_util.h"
+#include "base/mac/scoped_nsautorelease_pool.h"
+#include "base/sys_string_conversions.h"
+#import "base/worker_pool_mac.h"
+#include "base/ref_counted.h"
+#include "base/task.h"
+#include "base/worker_pool.h"
+#include "chrome/browser/ui/cocoa/authorization_util.h"
+#include "chrome/common/chrome_constants.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+
+namespace {
+
+// Provide declarations of the Keystone registration bits needed here. From
+// KSRegistration.h.
+typedef enum {
+ kKSPathExistenceChecker,
+} KSExistenceCheckerType;
+
+typedef enum {
+ kKSRegistrationUserTicket,
+ kKSRegistrationSystemTicket,
+ kKSRegistrationDontKnowWhatKindOfTicket,
+} KSRegistrationTicketType;
+
+NSString* const KSRegistrationVersionKey = @"Version";
+NSString* const KSRegistrationExistenceCheckerTypeKey = @"ExistenceCheckerType";
+NSString* const KSRegistrationExistenceCheckerStringKey =
+ @"ExistenceCheckerString";
+NSString* const KSRegistrationServerURLStringKey = @"URLString";
+NSString* const KSRegistrationPreserveTrustedTesterTokenKey = @"PreserveTTT";
+NSString* const KSRegistrationTagKey = @"Tag";
+NSString* const KSRegistrationTagPathKey = @"TagPath";
+NSString* const KSRegistrationTagKeyKey = @"TagKey";
+NSString* const KSRegistrationBrandPathKey = @"BrandPath";
+NSString* const KSRegistrationBrandKeyKey = @"BrandKey";
+
+NSString* const KSRegistrationDidCompleteNotification =
+ @"KSRegistrationDidCompleteNotification";
+NSString* const KSRegistrationPromotionDidCompleteNotification =
+ @"KSRegistrationPromotionDidCompleteNotification";
+
+NSString* const KSRegistrationCheckForUpdateNotification =
+ @"KSRegistrationCheckForUpdateNotification";
+NSString* KSRegistrationStatusKey = @"Status";
+NSString* KSRegistrationUpdateCheckErrorKey = @"Error";
+
+NSString* const KSRegistrationStartUpdateNotification =
+ @"KSRegistrationStartUpdateNotification";
+NSString* const KSUpdateCheckSuccessfulKey = @"CheckSuccessful";
+NSString* const KSUpdateCheckSuccessfullyInstalledKey =
+ @"SuccessfullyInstalled";
+
+NSString* const KSRegistrationRemoveExistingTag = @"";
+#define KSRegistrationPreserveExistingTag nil
+
+// Constants for the brand file (uses an external file so it can survive updates
+// to Chrome.
+
+#if defined(GOOGLE_CHROME_BUILD)
+#define kBrandFileName @"Google Chrome Brand.plist";
+#elif defined(CHROMIUM_BUILD)
+#define kBrandFileName @"Chromium Brand.plist";
+#else
+#error Unknown branding
+#endif
+
+// These directories are hardcoded in Keystone promotion preflight and the
+// Keystone install script, so NSSearchPathForDirectoriesInDomains isn't used
+// since the scripts couldn't use anything like that.
+NSString* kBrandUserFile = @"~/Library/Google/" kBrandFileName;
+NSString* kBrandSystemFile = @"/Library/Google/" kBrandFileName;
+
+NSString* UserBrandFilePath() {
+ return [kBrandUserFile stringByStandardizingPath];
+}
+NSString* SystemBrandFilePath() {
+ return [kBrandSystemFile stringByStandardizingPath];
+}
+
+// Adaptor for scheduling an Objective-C method call on a |WorkerPool|
+// thread.
+// TODO(shess): Move this into workerpool_mac.h?
+class PerformBridge : public base::RefCountedThreadSafe<PerformBridge> {
+ public:
+
+ // Call |sel| on |target| with |arg| in a WorkerPool thread.
+ // |target| and |arg| are retained, |arg| may be |nil|.
+ static void PostPerform(id target, SEL sel, id arg) {
+ DCHECK(target);
+ DCHECK(sel);
+
+ scoped_refptr<PerformBridge> op = new PerformBridge(target, sel, arg);
+ WorkerPool::PostTask(
+ FROM_HERE, NewRunnableMethod(op.get(), &PerformBridge::Run), true);
+ }
+
+ // Convenience for the no-argument case.
+ static void PostPerform(id target, SEL sel) {
+ PostPerform(target, sel, nil);
+ }
+
+ private:
+ // Allow RefCountedThreadSafe<> to delete.
+ friend class base::RefCountedThreadSafe<PerformBridge>;
+
+ PerformBridge(id target, SEL sel, id arg)
+ : target_([target retain]),
+ sel_(sel),
+ arg_([arg retain]) {
+ }
+
+ ~PerformBridge() {}
+
+ // Happens on a WorkerPool thread.
+ void Run() {
+ base::mac::ScopedNSAutoreleasePool pool;
+ [target_ performSelector:sel_ withObject:arg_];
+ }
+
+ scoped_nsobject<id> target_;
+ SEL sel_;
+ scoped_nsobject<id> arg_;
+};
+
+} // namespace
+
+@interface KSRegistration : NSObject
+
++ (id)registrationWithProductID:(NSString*)productID;
+
+- (BOOL)registerWithParameters:(NSDictionary*)args;
+
+- (BOOL)promoteWithParameters:(NSDictionary*)args
+ authorization:(AuthorizationRef)authorization;
+
+- (void)setActive;
+- (void)checkForUpdate;
+- (void)startUpdate;
+- (KSRegistrationTicketType)ticketType;
+
+@end // @interface KSRegistration
+
+@interface KeystoneGlue(Private)
+
+// Returns the path to the application's Info.plist file. This returns the
+// outer application bundle's Info.plist, not the framework's Info.plist.
+- (NSString*)appInfoPlistPath;
+
+// Returns a dictionary containing parameters to be used for a KSRegistration
+// -registerWithParameters: or -promoteWithParameters:authorization: call.
+- (NSDictionary*)keystoneParameters;
+
+// Called when Keystone registration completes.
+- (void)registrationComplete:(NSNotification*)notification;
+
+// Called periodically to announce activity by pinging the Keystone server.
+- (void)markActive:(NSTimer*)timer;
+
+// Called when an update check or update installation is complete. Posts the
+// kAutoupdateStatusNotification notification to the default notification
+// center.
+- (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version;
+
+// Returns the version of the currently-installed application on disk.
+- (NSString*)currentlyInstalledVersion;
+
+// These three methods are used to determine the version of the application
+// currently installed on disk, compare that to the currently-running version,
+// decide whether any updates have been installed, and call
+// -updateStatus:version:.
+//
+// In order to check the version on disk, the installed application's
+// Info.plist dictionary must be read; in order to see changes as updates are
+// applied, the dictionary must be read each time, bypassing any caches such
+// as the one that NSBundle might be maintaining. Reading files can be a
+// blocking operation, and blocking operations are to be avoided on the main
+// thread. I'm not quite sure what jank means, but I bet that a blocked main
+// thread would cause some of it.
+//
+// -determineUpdateStatusAsync is called on the main thread to initiate the
+// operation. It performs initial set-up work that must be done on the main
+// thread and arranges for -determineUpdateStatus to be called on a work queue
+// thread managed by WorkerPool.
+// -determineUpdateStatus then reads the Info.plist, gets the version from the
+// CFBundleShortVersionString key, and performs
+// -determineUpdateStatusForVersion: on the main thread.
+// -determineUpdateStatusForVersion: does the actual comparison of the version
+// on disk with the running version and calls -updateStatus:version: with the
+// results of its analysis.
+- (void)determineUpdateStatusAsync;
+- (void)determineUpdateStatus;
+- (void)determineUpdateStatusForVersion:(NSString*)version;
+
+// Returns YES if registration_ is definitely on a user ticket. If definitely
+// on a system ticket, or uncertain of ticket type (due to an older version
+// of Keystone being used), returns NO.
+- (BOOL)isUserTicket;
+
+// Called when ticket promotion completes.
+- (void)promotionComplete:(NSNotification*)notification;
+
+// Changes the application's ownership and permissions so that all files are
+// owned by root:wheel and all files and directories are writable only by
+// root, but readable and executable as needed by everyone.
+// -changePermissionsForPromotionAsync is called on the main thread by
+// -promotionComplete. That routine calls
+// -changePermissionsForPromotionWithTool: on a work queue thread. When done,
+// -changePermissionsForPromotionComplete is called on the main thread.
+- (void)changePermissionsForPromotionAsync;
+- (void)changePermissionsForPromotionWithTool:(NSString*)toolPath;
+- (void)changePermissionsForPromotionComplete;
+
+// Returns the brand file path to use for Keystone.
+- (NSString*)brandFilePath;
+
+@end // @interface KeystoneGlue(Private)
+
+NSString* const kAutoupdateStatusNotification = @"AutoupdateStatusNotification";
+NSString* const kAutoupdateStatusStatus = @"status";
+NSString* const kAutoupdateStatusVersion = @"version";
+
+namespace {
+
+NSString* const kChannelKey = @"KSChannelID";
+NSString* const kBrandKey = @"KSBrandID";
+
+} // namespace
+
+@implementation KeystoneGlue
+
++ (id)defaultKeystoneGlue {
+ static bool sTriedCreatingDefaultKeystoneGlue = false;
+ // TODO(jrg): use base::SingletonObjC<KeystoneGlue>
+ static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked
+
+ if (!sTriedCreatingDefaultKeystoneGlue) {
+ sTriedCreatingDefaultKeystoneGlue = true;
+
+ sDefaultKeystoneGlue = [[KeystoneGlue alloc] init];
+ [sDefaultKeystoneGlue loadParameters];
+ if (![sDefaultKeystoneGlue loadKeystoneRegistration]) {
+ [sDefaultKeystoneGlue release];
+ sDefaultKeystoneGlue = nil;
+ }
+ }
+ return sDefaultKeystoneGlue;
+}
+
+- (id)init {
+ if ((self = [super init])) {
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+
+ [center addObserver:self
+ selector:@selector(registrationComplete:)
+ name:KSRegistrationDidCompleteNotification
+ object:nil];
+
+ [center addObserver:self
+ selector:@selector(promotionComplete:)
+ name:KSRegistrationPromotionDidCompleteNotification
+ object:nil];
+
+ [center addObserver:self
+ selector:@selector(checkForUpdateComplete:)
+ name:KSRegistrationCheckForUpdateNotification
+ object:nil];
+
+ [center addObserver:self
+ selector:@selector(installUpdateComplete:)
+ name:KSRegistrationStartUpdateNotification
+ object:nil];
+ }
+
+ return self;
+}
+
+- (void)dealloc {
+ [productID_ release];
+ [appPath_ release];
+ [url_ release];
+ [version_ release];
+ [channel_ release];
+ [registration_ release];
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+- (NSDictionary*)infoDictionary {
+ // Use [NSBundle mainBundle] to get the application's own bundle identifier
+ // and path, not the framework's. For auto-update, the application is
+ // what's significant here: it's used to locate the outermost part of the
+ // application for the existence checker and other operations that need to
+ // see the entire application bundle.
+ return [[NSBundle mainBundle] infoDictionary];
+}
+
+- (void)loadParameters {
+ NSBundle* appBundle = [NSBundle mainBundle];
+ NSDictionary* infoDictionary = [self infoDictionary];
+
+ NSString* productID = [infoDictionary objectForKey:@"KSProductID"];
+ if (productID == nil) {
+ productID = [appBundle bundleIdentifier];
+ }
+
+ NSString* appPath = [appBundle bundlePath];
+ NSString* url = [infoDictionary objectForKey:@"KSUpdateURL"];
+ NSString* version = [infoDictionary objectForKey:@"KSVersion"];
+
+ if (!productID || !appPath || !url || !version) {
+ // If parameters required for Keystone are missing, don't use it.
+ return;
+ }
+
+ NSString* channel = [infoDictionary objectForKey:kChannelKey];
+ // The stable channel has no tag. If updating to stable, remove the
+ // dev and beta tags since we've been "promoted".
+ if (channel == nil)
+ channel = KSRegistrationRemoveExistingTag;
+
+ productID_ = [productID retain];
+ appPath_ = [appPath retain];
+ url_ = [url retain];
+ version_ = [version retain];
+ channel_ = [channel retain];
+}
+
+- (NSString*)brandFilePath {
+ DCHECK(version_ != nil) << "-loadParameters must be called first";
+
+ if (brandFileType_ == kBrandFileTypeNotDetermined) {
+
+ // Default to none.
+ brandFileType_ = kBrandFileTypeNone;
+
+ // Having a channel means Dev/Beta, so there is no brand code to go with
+ // those.
+ if ([channel_ length] == 0) {
+
+ NSString* userBrandFile = UserBrandFilePath();
+ NSString* systemBrandFile = SystemBrandFilePath();
+
+ NSFileManager* fm = [NSFileManager defaultManager];
+
+ // If there is a system brand file, use it.
+ if ([fm fileExistsAtPath:systemBrandFile]) {
+ // System
+
+ // Use the system file that is there.
+ brandFileType_ = kBrandFileTypeSystem;
+
+ // Clean up any old user level file.
+ if ([fm fileExistsAtPath:userBrandFile]) {
+ [fm removeItemAtPath:userBrandFile error:NULL];
+ }
+
+ } else {
+ // User
+
+ NSDictionary* infoDictionary = [self infoDictionary];
+ NSString* appBundleBrandID = [infoDictionary objectForKey:kBrandKey];
+
+ NSString* storedBrandID = nil;
+ if ([fm fileExistsAtPath:userBrandFile]) {
+ NSDictionary* storedBrandDict =
+ [NSDictionary dictionaryWithContentsOfFile:userBrandFile];
+ storedBrandID = [storedBrandDict objectForKey:kBrandKey];
+ }
+
+ if ((appBundleBrandID != nil) &&
+ (![storedBrandID isEqualTo:appBundleBrandID])) {
+ // App and store don't match, update store and use it.
+ NSDictionary* storedBrandDict =
+ [NSDictionary dictionaryWithObject:appBundleBrandID
+ forKey:kBrandKey];
+ // If Keystone hasn't been installed yet, the location the brand file
+ // is written to won't exist, so manually create the directory.
+ NSString *userBrandFileDirectory =
+ [userBrandFile stringByDeletingLastPathComponent];
+ if (![fm fileExistsAtPath:userBrandFileDirectory]) {
+ if (![fm createDirectoryAtPath:userBrandFileDirectory
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:NULL]) {
+ LOG(ERROR) << "Failed to create the directory for the brand file";
+ }
+ }
+ if ([storedBrandDict writeToFile:userBrandFile atomically:YES]) {
+ brandFileType_ = kBrandFileTypeUser;
+ }
+ } else if (storedBrandID) {
+ // Had stored brand, use it.
+ brandFileType_ = kBrandFileTypeUser;
+ }
+ }
+ }
+
+ }
+
+ NSString* result = nil;
+ switch (brandFileType_) {
+ case kBrandFileTypeUser:
+ result = UserBrandFilePath();
+ break;
+
+ case kBrandFileTypeSystem:
+ result = SystemBrandFilePath();
+ break;
+
+ case kBrandFileTypeNotDetermined:
+ NOTIMPLEMENTED();
+ // Fall through
+ case kBrandFileTypeNone:
+ // Clear the value.
+ result = @"";
+ break;
+
+ }
+ return result;
+}
+
+- (BOOL)loadKeystoneRegistration {
+ if (!productID_ || !appPath_ || !url_ || !version_)
+ return NO;
+
+ // Load the KeystoneRegistration framework bundle if present. It lives
+ // inside the framework, so use mac_util::MainAppBundle();
+ NSString* ksrPath =
+ [[mac_util::MainAppBundle() privateFrameworksPath]
+ stringByAppendingPathComponent:@"KeystoneRegistration.framework"];
+ NSBundle* ksrBundle = [NSBundle bundleWithPath:ksrPath];
+ [ksrBundle load];
+
+ // Harness the KSRegistration class.
+ Class ksrClass = [ksrBundle classNamed:@"KSRegistration"];
+ KSRegistration* ksr = [ksrClass registrationWithProductID:productID_];
+ if (!ksr)
+ return NO;
+
+ registration_ = [ksr retain];
+ return YES;
+}
+
+- (NSString*)appInfoPlistPath {
+ // NSBundle ought to have a way to access this path directly, but it
+ // doesn't.
+ return [[appPath_ stringByAppendingPathComponent:@"Contents"]
+ stringByAppendingPathComponent:@"Info.plist"];
+}
+
+- (NSDictionary*)keystoneParameters {
+ NSNumber* xcType = [NSNumber numberWithInt:kKSPathExistenceChecker];
+ NSNumber* preserveTTToken = [NSNumber numberWithBool:YES];
+ NSString* tagPath = [self appInfoPlistPath];
+
+ NSString* brandKey = kBrandKey;
+ NSString* brandPath = [self brandFilePath];
+
+ if ([brandPath length] == 0) {
+ // Brand path and brand key must be cleared together or ksadmin seems
+ // to throw an error.
+ brandKey = @"";
+ }
+
+ return [NSDictionary dictionaryWithObjectsAndKeys:
+ version_, KSRegistrationVersionKey,
+ xcType, KSRegistrationExistenceCheckerTypeKey,
+ appPath_, KSRegistrationExistenceCheckerStringKey,
+ url_, KSRegistrationServerURLStringKey,
+ preserveTTToken, KSRegistrationPreserveTrustedTesterTokenKey,
+ channel_, KSRegistrationTagKey,
+ tagPath, KSRegistrationTagPathKey,
+ kChannelKey, KSRegistrationTagKeyKey,
+ brandPath, KSRegistrationBrandPathKey,
+ brandKey, KSRegistrationBrandKeyKey,
+ nil];
+}
+
+- (void)registerWithKeystone {
+ [self updateStatus:kAutoupdateRegistering version:nil];
+
+ NSDictionary* parameters = [self keystoneParameters];
+ if (![registration_ registerWithParameters:parameters]) {
+ [self updateStatus:kAutoupdateRegisterFailed version:nil];
+ return;
+ }
+
+ // Upon completion, KSRegistrationDidCompleteNotification will be posted,
+ // and -registrationComplete: will be called.
+
+ // Mark an active RIGHT NOW; don't wait an hour for the first one.
+ [registration_ setActive];
+
+ // Set up hourly activity pings.
+ timer_ = [NSTimer scheduledTimerWithTimeInterval:60 * 60 // One hour
+ target:self
+ selector:@selector(markActive:)
+ userInfo:registration_
+ repeats:YES];
+}
+
+- (void)registrationComplete:(NSNotification*)notification {
+ NSDictionary* userInfo = [notification userInfo];
+ if ([[userInfo objectForKey:KSRegistrationStatusKey] boolValue]) {
+ [self updateStatus:kAutoupdateRegistered version:nil];
+ } else {
+ // Dump registration_?
+ [self updateStatus:kAutoupdateRegisterFailed version:nil];
+ }
+}
+
+- (void)stopTimer {
+ [timer_ invalidate];
+}
+
+- (void)markActive:(NSTimer*)timer {
+ KSRegistration* ksr = [timer userInfo];
+ [ksr setActive];
+}
+
+- (void)checkForUpdate {
+ DCHECK(![self asyncOperationPending]);
+
+ if (!registration_) {
+ [self updateStatus:kAutoupdateCheckFailed version:nil];
+ return;
+ }
+
+ [self updateStatus:kAutoupdateChecking version:nil];
+
+ [registration_ checkForUpdate];
+
+ // Upon completion, KSRegistrationCheckForUpdateNotification will be posted,
+ // and -checkForUpdateComplete: will be called.
+}
+
+- (void)checkForUpdateComplete:(NSNotification*)notification {
+ NSDictionary* userInfo = [notification userInfo];
+
+ if ([[userInfo objectForKey:KSRegistrationUpdateCheckErrorKey] boolValue]) {
+ [self updateStatus:kAutoupdateCheckFailed version:nil];
+ } else if ([[userInfo objectForKey:KSRegistrationStatusKey] boolValue]) {
+ // If an update is known to be available, go straight to
+ // -updateStatus:version:. It doesn't matter what's currently on disk.
+ NSString* version = [userInfo objectForKey:KSRegistrationVersionKey];
+ [self updateStatus:kAutoupdateAvailable version:version];
+ } else {
+ // If no updates are available, check what's on disk, because an update
+ // may have already been installed. This check happens on another thread,
+ // and -updateStatus:version: will be called on the main thread when done.
+ [self determineUpdateStatusAsync];
+ }
+}
+
+- (void)installUpdate {
+ DCHECK(![self asyncOperationPending]);
+
+ if (!registration_) {
+ [self updateStatus:kAutoupdateInstallFailed version:nil];
+ return;
+ }
+
+ [self updateStatus:kAutoupdateInstalling version:nil];
+
+ [registration_ startUpdate];
+
+ // Upon completion, KSRegistrationStartUpdateNotification will be posted,
+ // and -installUpdateComplete: will be called.
+}
+
+- (void)installUpdateComplete:(NSNotification*)notification {
+ NSDictionary* userInfo = [notification userInfo];
+
+ if (![[userInfo objectForKey:KSUpdateCheckSuccessfulKey] boolValue] ||
+ ![[userInfo objectForKey:KSUpdateCheckSuccessfullyInstalledKey]
+ intValue]) {
+ [self updateStatus:kAutoupdateInstallFailed version:nil];
+ } else {
+ updateSuccessfullyInstalled_ = YES;
+
+ // Nothing in the notification dictionary reports the version that was
+ // installed. Figure it out based on what's on disk.
+ [self determineUpdateStatusAsync];
+ }
+}
+
+- (NSString*)currentlyInstalledVersion {
+ NSString* appInfoPlistPath = [self appInfoPlistPath];
+ NSDictionary* infoPlist =
+ [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath];
+ return [infoPlist objectForKey:@"CFBundleShortVersionString"];
+}
+
+// Runs on the main thread.
+- (void)determineUpdateStatusAsync {
+ DCHECK([NSThread isMainThread]);
+
+ PerformBridge::PostPerform(self, @selector(determineUpdateStatus));
+}
+
+// Runs on a thread managed by WorkerPool.
+- (void)determineUpdateStatus {
+ DCHECK(![NSThread isMainThread]);
+
+ NSString* version = [self currentlyInstalledVersion];
+
+ [self performSelectorOnMainThread:@selector(determineUpdateStatusForVersion:)
+ withObject:version
+ waitUntilDone:NO];
+}
+
+// Runs on the main thread.
+- (void)determineUpdateStatusForVersion:(NSString*)version {
+ DCHECK([NSThread isMainThread]);
+
+ AutoupdateStatus status;
+ if (updateSuccessfullyInstalled_) {
+ // If an update was successfully installed and this object saw it happen,
+ // then don't even bother comparing versions.
+ status = kAutoupdateInstalled;
+ } else {
+ NSString* currentVersion =
+ [NSString stringWithUTF8String:chrome::kChromeVersion];
+ if (!version) {
+ // If the version on disk could not be determined, assume that
+ // whatever's running is current.
+ version = currentVersion;
+ status = kAutoupdateCurrent;
+ } else if ([version isEqualToString:currentVersion]) {
+ status = kAutoupdateCurrent;
+ } else {
+ // If the version on disk doesn't match what's currently running, an
+ // update must have been applied in the background, without this app's
+ // direct participation. Leave updateSuccessfullyInstalled_ alone
+ // because there's no direct knowledge of what actually happened.
+ status = kAutoupdateInstalled;
+ }
+ }
+
+ [self updateStatus:status version:version];
+}
+
+- (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version {
+ NSNumber* statusNumber = [NSNumber numberWithInt:status];
+ NSMutableDictionary* dictionary =
+ [NSMutableDictionary dictionaryWithObject:statusNumber
+ forKey:kAutoupdateStatusStatus];
+ if (version) {
+ [dictionary setObject:version forKey:kAutoupdateStatusVersion];
+ }
+
+ NSNotification* notification =
+ [NSNotification notificationWithName:kAutoupdateStatusNotification
+ object:self
+ userInfo:dictionary];
+ recentNotification_.reset([notification retain]);
+
+ [[NSNotificationCenter defaultCenter] postNotification:notification];
+}
+
+- (NSNotification*)recentNotification {
+ return [[recentNotification_ retain] autorelease];
+}
+
+- (AutoupdateStatus)recentStatus {
+ NSDictionary* dictionary = [recentNotification_ userInfo];
+ return static_cast<AutoupdateStatus>(
+ [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]);
+}
+
+- (BOOL)asyncOperationPending {
+ AutoupdateStatus status = [self recentStatus];
+ return status == kAutoupdateRegistering ||
+ status == kAutoupdateChecking ||
+ status == kAutoupdateInstalling ||
+ status == kAutoupdatePromoting;
+}
+
+- (BOOL)isUserTicket {
+ return [registration_ ticketType] == kKSRegistrationUserTicket;
+}
+
+- (BOOL)isOnReadOnlyFilesystem {
+ const char* appPathC = [appPath_ fileSystemRepresentation];
+ struct statfs statfsBuf;
+
+ if (statfs(appPathC, &statfsBuf) != 0) {
+ PLOG(ERROR) << "statfs";
+ // Be optimistic about the filesystem's writability.
+ return NO;
+ }
+
+ return (statfsBuf.f_flags & MNT_RDONLY) != 0;
+}
+
+- (BOOL)needsPromotion {
+ if (![self isUserTicket] || [self isOnReadOnlyFilesystem]) {
+ return NO;
+ }
+
+ // Check the outermost bundle directory, the main executable path, and the
+ // framework directory. It may be enough to just look at the outermost
+ // bundle directory, but checking an interior file and directory can be
+ // helpful in case permissions are set differently only on the outermost
+ // directory. An interior file and directory are both checked because some
+ // file operations, such as Snow Leopard's Finder's copy operation when
+ // authenticating, may actually result in different ownership being applied
+ // to files and directories.
+ NSFileManager* fileManager = [NSFileManager defaultManager];
+ NSString* executablePath = [[NSBundle mainBundle] executablePath];
+ NSString* frameworkPath = [mac_util::MainAppBundle() bundlePath];
+ return ![fileManager isWritableFileAtPath:appPath_] ||
+ ![fileManager isWritableFileAtPath:executablePath] ||
+ ![fileManager isWritableFileAtPath:frameworkPath];
+}
+
+- (BOOL)wantsPromotion {
+ // -needsPromotion checks these too, but this method doesn't necessarily
+ // return NO just becuase -needsPromotion returns NO, so another check is
+ // needed here.
+ if (![self isUserTicket] || [self isOnReadOnlyFilesystem]) {
+ return NO;
+ }
+
+ if ([self needsPromotion]) {
+ return YES;
+ }
+
+ return [appPath_ hasPrefix:@"/Applications/"];
+}
+
+- (void)promoteTicket {
+ if ([self asyncOperationPending] || ![self wantsPromotion]) {
+ // Because there are multiple ways of reaching promoteTicket that might
+ // not lock each other out, it may be possible to arrive here while an
+ // asynchronous operation is pending, or even after promotion has already
+ // occurred. Just quietly return without doing anything.
+ return;
+ }
+
+ NSString* prompt = l10n_util::GetNSStringFWithFixup(
+ IDS_PROMOTE_AUTHENTICATION_PROMPT,
+ l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
+ scoped_AuthorizationRef authorization(
+ authorization_util::AuthorizationCreateToRunAsRoot(
+ reinterpret_cast<CFStringRef>(prompt)));
+ if (!authorization.get()) {
+ return;
+ }
+
+ [self promoteTicketWithAuthorization:authorization.release() synchronous:NO];
+}
+
+- (void)promoteTicketWithAuthorization:(AuthorizationRef)authorization_arg
+ synchronous:(BOOL)synchronous {
+ scoped_AuthorizationRef authorization(authorization_arg);
+ authorization_arg = NULL;
+
+ if ([self asyncOperationPending]) {
+ // Starting a synchronous operation while an asynchronous one is pending
+ // could be trouble.
+ return;
+ }
+ if (!synchronous && ![self wantsPromotion]) {
+ // If operating synchronously, the call came from the installer, which
+ // means that a system ticket is required. Otherwise, only allow
+ // promotion if it's wanted.
+ return;
+ }
+
+ synchronousPromotion_ = synchronous;
+
+ [self updateStatus:kAutoupdatePromoting version:nil];
+
+ // TODO(mark): Remove when able!
+ //
+ // keystone_promote_preflight will copy the current brand information out to
+ // the system level so all users can share the data as part of the ticket
+ // promotion.
+ //
+ // It will also ensure that the Keystone system ticket store is in a usable
+ // state for all users on the system. Ideally, Keystone's installer or
+ // another part of Keystone would handle this. The underlying problem is
+ // http://b/2285921, and it causes http://b/2289908, which this workaround
+ // addresses.
+ //
+ // This is run synchronously, which isn't optimal, but
+ // -[KSRegistration promoteWithParameters:authorization:] is currently
+ // synchronous too, and this operation needs to happen before that one.
+ //
+ // TODO(mark): Make asynchronous. That only makes sense if the promotion
+ // operation itself is asynchronous too. http://b/2290009. Hopefully,
+ // the Keystone promotion code will just be changed to do what preflight
+ // now does, and then the preflight script can be removed instead.
+ // However, preflight operation (and promotion) should only be asynchronous
+ // if the synchronous parameter is NO.
+ NSString* preflightPath =
+ [mac_util::MainAppBundle() pathForResource:@"keystone_promote_preflight"
+ ofType:@"sh"];
+ const char* preflightPathC = [preflightPath fileSystemRepresentation];
+ const char* userBrandFile = NULL;
+ const char* systemBrandFile = NULL;
+ if (brandFileType_ == kBrandFileTypeUser) {
+ // Running with user level brand file, promote to the system level.
+ userBrandFile = [UserBrandFilePath() fileSystemRepresentation];
+ systemBrandFile = [SystemBrandFilePath() fileSystemRepresentation];
+ }
+ const char* arguments[] = {userBrandFile, systemBrandFile, NULL};
+
+ int exit_status;
+ OSStatus status = authorization_util::ExecuteWithPrivilegesAndWait(
+ authorization,
+ preflightPathC,
+ kAuthorizationFlagDefaults,
+ arguments,
+ NULL, // pipe
+ &exit_status);
+ if (status != errAuthorizationSuccess) {
+ LOG(ERROR) << "AuthorizationExecuteWithPrivileges preflight: " << status;
+ [self updateStatus:kAutoupdatePromoteFailed version:nil];
+ return;
+ }
+ if (exit_status != 0) {
+ LOG(ERROR) << "keystone_promote_preflight status " << exit_status;
+ [self updateStatus:kAutoupdatePromoteFailed version:nil];
+ return;
+ }
+
+ // Hang on to the AuthorizationRef so that it can be used once promotion is
+ // complete. Do this before asking Keystone to promote the ticket, because
+ // -promotionComplete: may be called from inside the Keystone promotion
+ // call.
+ authorization_.swap(authorization);
+
+ NSDictionary* parameters = [self keystoneParameters];
+
+ // If the brand file is user level, update parameters to point to the new
+ // system level file during promotion.
+ if (brandFileType_ == kBrandFileTypeUser) {
+ NSMutableDictionary* temp_parameters =
+ [[parameters mutableCopy] autorelease];
+ [temp_parameters setObject:SystemBrandFilePath()
+ forKey:KSRegistrationBrandPathKey];
+ parameters = temp_parameters;
+ }
+
+ if (![registration_ promoteWithParameters:parameters
+ authorization:authorization_]) {
+ [self updateStatus:kAutoupdatePromoteFailed version:nil];
+ authorization_.reset();
+ return;
+ }
+
+ // Upon completion, KSRegistrationPromotionDidCompleteNotification will be
+ // posted, and -promotionComplete: will be called.
+}
+
+- (void)promotionComplete:(NSNotification*)notification {
+ NSDictionary* userInfo = [notification userInfo];
+ if ([[userInfo objectForKey:KSRegistrationStatusKey] boolValue]) {
+ if (synchronousPromotion_) {
+ // Short-circuit: if performing a synchronous promotion, the promotion
+ // came from the installer, which already set the permissions properly.
+ // Rather than run a duplicate permission-changing operation, jump
+ // straight to "done."
+ [self changePermissionsForPromotionComplete];
+ } else {
+ [self changePermissionsForPromotionAsync];
+ }
+ } else {
+ authorization_.reset();
+ [self updateStatus:kAutoupdatePromoteFailed version:nil];
+ }
+}
+
+- (void)changePermissionsForPromotionAsync {
+ // NSBundle is not documented as being thread-safe. Do NSBundle operations
+ // on the main thread before jumping over to a WorkerPool-managed
+ // thread to run the tool.
+ DCHECK([NSThread isMainThread]);
+
+ SEL selector = @selector(changePermissionsForPromotionWithTool:);
+ NSString* toolPath =
+ [mac_util::MainAppBundle() pathForResource:@"keystone_promote_postflight"
+ ofType:@"sh"];
+
+ PerformBridge::PostPerform(self, selector, toolPath);
+}
+
+- (void)changePermissionsForPromotionWithTool:(NSString*)toolPath {
+ const char* toolPathC = [toolPath fileSystemRepresentation];
+
+ const char* appPathC = [appPath_ fileSystemRepresentation];
+ const char* arguments[] = {appPathC, NULL};
+
+ int exit_status;
+ OSStatus status = authorization_util::ExecuteWithPrivilegesAndWait(
+ authorization_,
+ toolPathC,
+ kAuthorizationFlagDefaults,
+ arguments,
+ NULL, // pipe
+ &exit_status);
+ if (status != errAuthorizationSuccess) {
+ LOG(ERROR) << "AuthorizationExecuteWithPrivileges postflight: " << status;
+ } else if (exit_status != 0) {
+ LOG(ERROR) << "keystone_promote_postflight status " << exit_status;
+ }
+
+ SEL selector = @selector(changePermissionsForPromotionComplete);
+ [self performSelectorOnMainThread:selector
+ withObject:nil
+ waitUntilDone:NO];
+}
+
+- (void)changePermissionsForPromotionComplete {
+ authorization_.reset();
+
+ [self updateStatus:kAutoupdatePromoted version:nil];
+}
+
+- (void)setAppPath:(NSString*)appPath {
+ if (appPath != appPath_) {
+ [appPath_ release];
+ appPath_ = [appPath copy];
+ }
+}
+
+@end // @implementation KeystoneGlue
+
+namespace keystone_glue {
+
+bool KeystoneEnabled() {
+ return [KeystoneGlue defaultKeystoneGlue] != nil;
+}
+
+string16 CurrentlyInstalledVersion() {
+ KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue];
+ NSString* version = [keystoneGlue currentlyInstalledVersion];
+ return base::SysNSStringToUTF16(version);
+}
+
+} // namespace keystone_glue
diff --git a/chrome/browser/ui/cocoa/keystone_glue_unittest.mm b/chrome/browser/ui/cocoa/keystone_glue_unittest.mm
new file mode 100644
index 0000000..c4a26f4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/keystone_glue_unittest.mm
@@ -0,0 +1,184 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Foundation/Foundation.h>
+#import <objc/objc-class.h>
+
+#import "chrome/browser/ui/cocoa/keystone_glue.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface FakeGlueRegistration : NSObject
+@end
+
+
+@implementation FakeGlueRegistration
+
+// Send the notifications that a real KeystoneGlue object would send.
+
+- (void)checkForUpdate {
+ NSNumber* yesNumber = [NSNumber numberWithBool:YES];
+ NSString* statusKey = @"Status";
+ NSDictionary* dictionary = [NSDictionary dictionaryWithObject:yesNumber
+ forKey:statusKey];
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center postNotificationName:@"KSRegistrationCheckForUpdateNotification"
+ object:nil
+ userInfo:dictionary];
+}
+
+- (void)startUpdate {
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center postNotificationName:@"KSRegistrationStartUpdateNotification"
+ object:nil];
+}
+
+@end
+
+
+@interface FakeKeystoneGlue : KeystoneGlue {
+ @public
+ BOOL upToDate_;
+ NSString *latestVersion_;
+ BOOL successful_;
+ int installs_;
+}
+
+- (void)fakeAboutWindowCallback:(NSNotification*)notification;
+@end
+
+
+@implementation FakeKeystoneGlue
+
+- (id)init {
+ if ((self = [super init])) {
+ // some lies
+ upToDate_ = YES;
+ latestVersion_ = @"foo bar";
+ successful_ = YES;
+ installs_ = 1010101010;
+
+ // Set up an observer that takes the notification that the About window
+ // listens for.
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
+ selector:@selector(fakeAboutWindowCallback:)
+ name:kAutoupdateStatusNotification
+ object:nil];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+// For mocking
+- (NSDictionary*)infoDictionary {
+ NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:
+ @"http://foo.bar", @"KSUpdateURL",
+ @"com.google.whatever", @"KSProductID",
+ @"0.0.0.1", @"KSVersion",
+ nil];
+ return dict;
+}
+
+// For mocking
+- (BOOL)loadKeystoneRegistration {
+ return YES;
+}
+
+// Confirms certain things are happy
+- (BOOL)dictReadCorrectly {
+ return ([url_ isEqual:@"http://foo.bar"] &&
+ [productID_ isEqual:@"com.google.whatever"] &&
+ [version_ isEqual:@"0.0.0.1"]);
+}
+
+// Confirms certain things are happy
+- (BOOL)hasATimer {
+ return timer_ ? YES : NO;
+}
+
+- (void)addFakeRegistration {
+ registration_ = [[FakeGlueRegistration alloc] init];
+}
+
+- (void)fakeAboutWindowCallback:(NSNotification*)notification {
+ NSDictionary* dictionary = [notification userInfo];
+ AutoupdateStatus status = static_cast<AutoupdateStatus>(
+ [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]);
+
+ if (status == kAutoupdateAvailable) {
+ upToDate_ = NO;
+ latestVersion_ = [dictionary objectForKey:kAutoupdateStatusVersion];
+ } else if (status == kAutoupdateInstallFailed) {
+ successful_ = NO;
+ installs_ = 0;
+ }
+}
+
+// Confirm we look like callbacks with nil NSNotifications
+- (BOOL)confirmCallbacks {
+ return (!upToDate_ &&
+ (latestVersion_ == nil) &&
+ !successful_ &&
+ (installs_ == 0));
+}
+
+@end
+
+
+namespace {
+
+class KeystoneGlueTest : public PlatformTest {
+};
+
+// DISABLED because the mocking isn't currently working.
+TEST_F(KeystoneGlueTest, DISABLED_BasicGlobalCreate) {
+ // Allow creation of a KeystoneGlue by mocking out a few calls
+ SEL ids = @selector(infoDictionary);
+ IMP oldInfoImp_ = [[KeystoneGlue class] instanceMethodForSelector:ids];
+ IMP newInfoImp_ = [[FakeKeystoneGlue class] instanceMethodForSelector:ids];
+ Method infoMethod_ = class_getInstanceMethod([KeystoneGlue class], ids);
+ method_setImplementation(infoMethod_, newInfoImp_);
+
+ SEL lks = @selector(loadKeystoneRegistration);
+ IMP oldLoadImp_ = [[KeystoneGlue class] instanceMethodForSelector:lks];
+ IMP newLoadImp_ = [[FakeKeystoneGlue class] instanceMethodForSelector:lks];
+ Method loadMethod_ = class_getInstanceMethod([KeystoneGlue class], lks);
+ method_setImplementation(loadMethod_, newLoadImp_);
+
+ KeystoneGlue *glue = [KeystoneGlue defaultKeystoneGlue];
+ ASSERT_TRUE(glue);
+
+ // Fix back up the class to the way we found it.
+ method_setImplementation(infoMethod_, oldInfoImp_);
+ method_setImplementation(loadMethod_, oldLoadImp_);
+}
+
+// DISABLED because the mocking isn't currently working.
+TEST_F(KeystoneGlueTest, DISABLED_BasicUse) {
+ FakeKeystoneGlue* glue = [[[FakeKeystoneGlue alloc] init] autorelease];
+ [glue loadParameters];
+ ASSERT_TRUE([glue dictReadCorrectly]);
+
+ // Likely returns NO in the unit test, but call it anyway to make
+ // sure it doesn't crash.
+ [glue loadKeystoneRegistration];
+
+ // Confirm we start up an active timer
+ [glue registerWithKeystone];
+ ASSERT_TRUE([glue hasATimer]);
+ [glue stopTimer];
+
+ // Brief exercise of callbacks
+ [glue addFakeRegistration];
+ [glue checkForUpdate];
+ [glue installUpdate];
+ ASSERT_TRUE([glue confirmCallbacks]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/keystone_infobar.h b/chrome/browser/ui/cocoa/keystone_infobar.h
new file mode 100644
index 0000000..61596ac
--- /dev/null
+++ b/chrome/browser/ui/cocoa/keystone_infobar.h
@@ -0,0 +1,24 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_KEYSTONE_INFOBAR_H_
+#define CHROME_BROWSER_UI_COCOA_KEYSTONE_INFOBAR_H_
+#pragma once
+
+class Profile;
+
+class KeystoneInfoBar {
+ public:
+ // If the application is Keystone-enabled and not on a read-only filesystem
+ // (capable of being auto-updated), and Keystone indicates that it needs
+ // ticket promotion, PromotionInfoBar displays an info bar asking the user
+ // to promote the ticket. The user will need to authenticate in order to
+ // gain authorization to perform the promotion. The info bar is not shown
+ // if its "don't ask" button was ever clicked, if the "don't check default
+ // browser" command-line flag is present, on the very first launch, or if
+ // another info bar is already showing in the active tab.
+ static void PromotionInfoBar(Profile* profile);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_KEYSTONE_INFOBAR_H_
diff --git a/chrome/browser/ui/cocoa/keystone_infobar.mm b/chrome/browser/ui/cocoa/keystone_infobar.mm
new file mode 100644
index 0000000..60635d6
--- /dev/null
+++ b/chrome/browser/ui/cocoa/keystone_infobar.mm
@@ -0,0 +1,212 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/keystone_infobar.h"
+
+#import <AppKit/AppKit.h>
+
+#include <string>
+
+#include "app/l10n_util.h"
+#include "app/resource_bundle.h"
+#include "base/command_line.h"
+#include "base/message_loop.h"
+#include "base/task.h"
+#include "chrome/browser/first_run/first_run.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/tab_contents/infobar_delegate.h"
+#include "chrome/browser/tab_contents/navigation_controller.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list.h"
+#import "chrome/browser/ui/cocoa/keystone_glue.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/common/pref_names.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+
+class SkBitmap;
+
+namespace {
+
+class KeystonePromotionInfoBarDelegate : public ConfirmInfoBarDelegate {
+ public:
+ KeystonePromotionInfoBarDelegate(TabContents* tab_contents)
+ : ConfirmInfoBarDelegate(tab_contents),
+ profile_(tab_contents->profile()),
+ can_expire_(false),
+ ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)) {
+ const int kCanExpireOnNavigationAfterMilliseconds = 8 * 1000;
+ MessageLoop::current()->PostDelayedTask(
+ FROM_HERE,
+ method_factory_.NewRunnableMethod(
+ &KeystonePromotionInfoBarDelegate::SetCanExpire),
+ kCanExpireOnNavigationAfterMilliseconds);
+ }
+
+ virtual ~KeystonePromotionInfoBarDelegate() {}
+
+ // Inherited from InfoBarDelegate and overridden.
+
+ virtual bool ShouldExpire(
+ const NavigationController::LoadCommittedDetails& details) {
+ return can_expire_;
+ }
+
+ virtual void InfoBarClosed() {
+ delete this;
+ }
+
+ // Inherited from AlertInfoBarDelegate and overridden.
+
+ virtual string16 GetMessageText() const {
+ return l10n_util::GetStringFUTF16(IDS_PROMOTE_INFOBAR_TEXT,
+ l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
+ }
+
+ virtual SkBitmap* GetIcon() const {
+ return ResourceBundle::GetSharedInstance().GetBitmapNamed(
+ IDR_PRODUCT_ICON_32);
+ }
+
+ // Inherited from ConfirmInfoBarDelegate and overridden.
+
+ virtual int GetButtons() const {
+ return BUTTON_OK | BUTTON_CANCEL | BUTTON_OK_DEFAULT;
+ }
+
+ virtual string16 GetButtonLabel(InfoBarButton button) const {
+ return button == BUTTON_OK ?
+ l10n_util::GetStringUTF16(IDS_PROMOTE_INFOBAR_PROMOTE_BUTTON) :
+ l10n_util::GetStringUTF16(IDS_PROMOTE_INFOBAR_DONT_ASK_BUTTON);
+ }
+
+ virtual bool Accept() {
+ [[KeystoneGlue defaultKeystoneGlue] promoteTicket];
+ return true;
+ }
+
+ virtual bool Cancel() {
+ profile_->GetPrefs()->SetBoolean(prefs::kShowUpdatePromotionInfoBar, false);
+ return true;
+ }
+
+ private:
+ // Sets this info bar to be able to expire. Called a predetermined amount
+ // of time after this object is created.
+ void SetCanExpire() {
+ can_expire_ = true;
+ }
+
+ // The TabContents' profile.
+ Profile* profile_; // weak
+
+ // Whether the info bar should be dismissed on the next navigation.
+ bool can_expire_;
+
+ // Used to delay the expiration of the info bar.
+ ScopedRunnableMethodFactory<KeystonePromotionInfoBarDelegate> method_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(KeystonePromotionInfoBarDelegate);
+};
+
+} // namespace
+
+@interface KeystonePromotionInfoBar : NSObject
+- (void)checkAndShowInfoBarForProfile:(Profile*)profile;
+- (void)updateStatus:(NSNotification*)notification;
+- (void)removeObserver;
+@end // @interface KeystonePromotionInfoBar
+
+@implementation KeystonePromotionInfoBar
+
+- (void)dealloc {
+ [self removeObserver];
+ [super dealloc];
+}
+
+- (void)checkAndShowInfoBarForProfile:(Profile*)profile {
+ // If this is the first run, the user clicked the "don't ask again" button
+ // at some point in the past, or if the "don't ask about the default
+ // browser" command-line switch is present, bail out. That command-line
+ // switch is recycled here because it's likely that the set of users that
+ // don't want to be nagged about the default browser also don't want to be
+ // nagged about the update check. (Automated testers, I'm thinking of
+ // you...)
+ CommandLine* commandLine = CommandLine::ForCurrentProcess();
+ if (FirstRun::IsChromeFirstRun() ||
+ !profile->GetPrefs()->GetBoolean(prefs::kShowUpdatePromotionInfoBar) ||
+ commandLine->HasSwitch(switches::kNoDefaultBrowserCheck)) {
+ return;
+ }
+
+ // If there is no Keystone glue (maybe because this application isn't
+ // Keystone-enabled) or the application is on a read-only filesystem,
+ // doing anything related to auto-update is pointless. Bail out.
+ KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue];
+ if (!keystoneGlue || [keystoneGlue isOnReadOnlyFilesystem]) {
+ return;
+ }
+
+ // Stay alive as long as needed. This is balanced by a release in
+ // -updateStatus:.
+ [self retain];
+
+ AutoupdateStatus recentStatus = [keystoneGlue recentStatus];
+ if (recentStatus == kAutoupdateNone ||
+ recentStatus == kAutoupdateRegistering) {
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
+ selector:@selector(updateStatus:)
+ name:kAutoupdateStatusNotification
+ object:nil];
+ } else {
+ [self updateStatus:[keystoneGlue recentNotification]];
+ }
+}
+
+- (void)updateStatus:(NSNotification*)notification {
+ NSDictionary* dictionary = [notification userInfo];
+ AutoupdateStatus status = static_cast<AutoupdateStatus>(
+ [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]);
+
+ if (status == kAutoupdateNone || status == kAutoupdateRegistering) {
+ return;
+ }
+
+ [self removeObserver];
+
+ if (status != kAutoupdateRegisterFailed &&
+ [[KeystoneGlue defaultKeystoneGlue] needsPromotion]) {
+ Browser* browser = BrowserList::GetLastActive();
+ if (browser) {
+ TabContents* tabContents = browser->GetSelectedTabContents();
+
+ // Only show if no other info bars are showing, because that's how the
+ // default browser info bar works.
+ if (tabContents && tabContents->infobar_delegate_count() == 0) {
+ tabContents->AddInfoBar(
+ new KeystonePromotionInfoBarDelegate(tabContents));
+ }
+ }
+ }
+
+ [self release];
+}
+
+- (void)removeObserver {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+@end // @implementation KeystonePromotionInfoBar
+
+// static
+void KeystoneInfoBar::PromotionInfoBar(Profile* profile) {
+ KeystonePromotionInfoBar* promotionInfoBar =
+ [[[KeystonePromotionInfoBar alloc] init] autorelease];
+
+ [promotionInfoBar checkAndShowInfoBarForProfile:profile];
+}
diff --git a/chrome/browser/ui/cocoa/keystone_promote_postflight.sh b/chrome/browser/ui/cocoa/keystone_promote_postflight.sh
new file mode 100755
index 0000000..44799f8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/keystone_promote_postflight.sh
@@ -0,0 +1,55 @@
+#!/bin/bash -p
+
+# Copyright (c) 2009 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# Called as root after Keystone ticket promotion to change the owner, group,
+# and permissions on the application. The application bundle and its contents
+# are set to owner root, group wheel, and to be writable only by root, but
+# readable and executable (when appropriate) by everyone.
+#
+# Note that this script will be invoked with the real user ID set to the
+# user's ID, but the effective user ID set to 0 (root). bash -p is used on
+# the first line to prevent bash from setting the effective user ID to the
+# real user ID (dropping root privileges).
+#
+# WARNING: This script is NOT currently run when the Keystone ticket is
+# promoted during application installation directly from the disk image,
+# because the installation process itself handles the same permission fix-ups
+# that this script normally would.
+
+set -e
+
+# This script runs as root, so be paranoid about things like ${PATH}.
+export PATH="/usr/bin:/usr/sbin:/bin:/sbin"
+
+# Output the pid to stdout before doing anything else. See
+# chrome/browser/ui/cocoa/authorization_util.h.
+echo "${$}"
+
+if [ ${#} -ne 1 ] ; then
+ echo "usage: ${0} APP" >& 2
+ exit 2
+fi
+
+APP="${1}"
+
+# Make sure that APP is an absolute path and that it exists.
+if [ -z "${APP}" ] || [ "${APP:0:1}" != "/" ] || [ ! -d "${APP}" ] ; then
+ echo "${0}: must provide an absolute path naming an extant directory" >& 2
+ exit 3
+fi
+
+OWNER_GROUP="root:wheel"
+chown -Rh "${OWNER_GROUP}" "${APP}" >& /dev/null
+
+CHMOD_MODE="a+rX,u+w,go-w"
+chmod -R "${CHMOD_MODE}" "${APP}" >& /dev/null
+
+# On the Mac, or at least on HFS+, symbolic link permissions are significant,
+# but chmod -R and -h can't be used together. Do another pass to fix the
+# permissions on any symbolic links.
+find "${APP}" -type l -exec chmod -h "${CHMOD_MODE}" {} + >& /dev/null
+
+exit 0
diff --git a/chrome/browser/ui/cocoa/keystone_promote_preflight.sh b/chrome/browser/ui/cocoa/keystone_promote_preflight.sh
new file mode 100755
index 0000000..4bf31e8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/keystone_promote_preflight.sh
@@ -0,0 +1,97 @@
+#!/bin/bash -p
+
+# Copyright (c) 2009 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# Called as root before Keystone ticket promotion to ensure a suitable
+# environment for Keystone installation. Ultimately, these features should be
+# integrated directly into the Keystone installation.
+#
+# If the two branding paths are given, then the branding information is also
+# copied and the permissions on the system branding file are set to be owned by
+# root, but readable by anyone.
+#
+# Note that this script will be invoked with the real user ID set to the
+# user's ID, but the effective user ID set to 0 (root). bash -p is used on
+# the first line to prevent bash from setting the effective user ID to the
+# real user ID (dropping root privileges).
+#
+# TODO(mark): Remove this script when able. See http://b/2285921 and
+# http://b/2289908.
+
+set -e
+
+# This script runs as root, so be paranoid about things like ${PATH}.
+export PATH="/usr/bin:/usr/sbin:/bin:/sbin"
+
+# Output the pid to stdout before doing anything else. See
+# chrome/browser/cocoa/authorization_util.h.
+echo "${$}"
+
+if [ ${#} -ne 0 ] && [ ${#} -ne 2 ] ; then
+ echo "usage: ${0} [USER_BRAND SYSTEM_BRAND]" >& 2
+ exit 2
+fi
+
+if [ ${#} -eq 2 ] ; then
+ USER_BRAND="${1}"
+ SYSTEM_BRAND="${2}"
+
+ # Make sure that USER_BRAND is an absolute path and that it exists.
+ if [ -z "${USER_BRAND}" ] || \
+ [ "${USER_BRAND:0:1}" != "/" ] || \
+ [ ! -f "${USER_BRAND}" ] ; then
+ echo "${0}: must provide an absolute path naming an existing user file" >& 2
+ exit 3
+ fi
+
+ # Make sure that SYSTEM_BRAND is an absolute path.
+ if [ -z "${SYSTEM_BRAND}" ] || [ "${SYSTEM_BRAND:0:1}" != "/" ] ; then
+ echo "${0}: must provide an absolute path naming a system file" >& 2
+ exit 4
+ fi
+
+ # Make sure the directory for the system brand file exists.
+ SYSTEM_BRAND_DIR=$(dirname "${SYSTEM_BRAND}")
+ if [ ! -e "${SYSTEM_BRAND_DIR}" ] ; then
+ mkdir -p "${SYSTEM_BRAND_DIR}"
+ # Permissions on this directory will be fixed up at the end of this script.
+ fi
+
+ # Copy the brand file
+ cp "${USER_BRAND}" "${SYSTEM_BRAND}" >& /dev/null
+
+ # Ensure the right ownership and permissions
+ chown "root:wheel" "${SYSTEM_BRAND}" >& /dev/null
+ chmod "a+r,u+w,go-w" "${SYSTEM_BRAND}" >& /dev/null
+
+fi
+
+OWNER_GROUP="root:admin"
+CHMOD_MODE="a+rX,u+w,go-w"
+
+LIB_GOOG="/Library/Google"
+if [ -d "${LIB_GOOG}" ] ; then
+ # Just work with the directory. Don't do anything recursively here, so as
+ # to leave other things in /Library/Google alone.
+ chown -h "${OWNER_GROUP}" "${LIB_GOOG}" >& /dev/null
+ chmod -h "${CHMOD_MODE}" "${LIB_GOOG}" >& /dev/null
+
+ LIB_GOOG_GSU="${LIB_GOOG}/GoogleSoftwareUpdate"
+ if [ -d "${LIB_GOOG_GSU}" ] ; then
+ chown -Rh "${OWNER_GROUP}" "${LIB_GOOG_GSU}" >& /dev/null
+ chmod -R "${CHMOD_MODE}" "${LIB_GOOG_GSU}" >& /dev/null
+
+ # On the Mac, or at least on HFS+, symbolic link permissions are
+ # significant, but chmod -R and -h can't be used together. Do another
+ # pass to fix the permissions on any symbolic links.
+ find "${LIB_GOOG_GSU}" -type l -exec chmod -h "${CHMOD_MODE}" {} + >& \
+ /dev/null
+
+ # TODO(mark): If GoogleSoftwareUpdate.bundle is missing, dump TicketStore
+ # too?
+ fi
+fi
+
+exit 0
diff --git a/chrome/browser/ui/cocoa/keyword_editor_cocoa_controller.h b/chrome/browser/ui/cocoa/keyword_editor_cocoa_controller.h
new file mode 100644
index 0000000..0934f0d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/keyword_editor_cocoa_controller.h
@@ -0,0 +1,117 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "app/table_model_observer.h"
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_ptr.h"
+#include "base/string16.h"
+#include "chrome/browser/search_engines/edit_search_engine_controller.h"
+#include "chrome/browser/search_engines/keyword_editor_controller.h"
+#include "chrome/browser/search_engines/template_url_model_observer.h"
+#include "chrome/browser/ui/cocoa/table_row_nsimage_cache.h"
+
+class EditSearchEngineControllerDelegate;
+@class KeywordEditorCocoaController;
+class Profile;
+@class WindowSizeAutosaver;
+
+// Very thin bridge that simply pushes notifications from C++ to ObjC.
+class KeywordEditorModelObserver : public TemplateURLModelObserver,
+ public EditSearchEngineControllerDelegate,
+ public TableModelObserver,
+ public TableRowNSImageCache::Table {
+ public:
+ explicit KeywordEditorModelObserver(KeywordEditorCocoaController* controller);
+ virtual ~KeywordEditorModelObserver();
+
+ // Notification that the template url model has changed in some way.
+ virtual void OnTemplateURLModelChanged();
+
+ // Invoked from the EditSearchEngineController when the user accepts the
+ // edits. NOTE: |template_url| is the value supplied to
+ // EditSearchEngineController's constructor, and may be NULL. A NULL value
+ // indicates a new TemplateURL should be created rather than modifying an
+ // existing TemplateURL.
+ virtual void OnEditedKeyword(const TemplateURL* template_url,
+ const string16& title,
+ const string16& keyword,
+ const std::string& url);
+
+ // TableModelObserver overrides. Invalidate icon cache.
+ virtual void OnModelChanged();
+ virtual void OnItemsChanged(int start, int length);
+ virtual void OnItemsAdded(int start, int length);
+ virtual void OnItemsRemoved(int start, int length);
+
+ // TableRowNSImageCache::Table
+ virtual int RowCount() const;
+ virtual SkBitmap GetIcon(int row) const;
+
+ // Lazily converts the image at the given row and caches it in |icon_cache_|.
+ NSImage* GetImageForRow(int row);
+
+ private:
+ KeywordEditorCocoaController* controller_;
+
+ TableRowNSImageCache icon_cache_;
+
+ DISALLOW_COPY_AND_ASSIGN(KeywordEditorModelObserver);
+};
+
+// This controller manages a window with a table view of search engines. It
+// acts as |tableView_|'s data source and delegate, feeding it data from the
+// KeywordEditorController's |table_model()|.
+
+@interface KeywordEditorCocoaController : NSWindowController
+ <NSWindowDelegate,
+ NSTableViewDataSource,
+ NSTableViewDelegate> {
+ IBOutlet NSTableView* tableView_;
+ IBOutlet NSButton* addButton_;
+ IBOutlet NSButton* removeButton_;
+ IBOutlet NSButton* makeDefaultButton_;
+
+ scoped_nsobject<NSTextFieldCell> groupCell_;
+
+ Profile* profile_; // weak
+ scoped_ptr<KeywordEditorController> controller_;
+ scoped_ptr<KeywordEditorModelObserver> observer_;
+
+ scoped_nsobject<WindowSizeAutosaver> sizeSaver_;
+}
+@property (nonatomic, readonly) KeywordEditorController* controller;
+
+// Show the keyword editor associated with the given profile (or the
+// original profile if this is an incognito profile). If no keyword
+// editor exists for this profile, create one and show it. Any
+// resulting editor releases itself when closed.
++ (void)showKeywordEditor:(Profile*)profile;
+
+- (KeywordEditorController*)controller;
+
+// Message forwarded by KeywordEditorModelObserver.
+- (void)modelChanged;
+
+- (IBAction)addKeyword:(id)sender;
+- (IBAction)deleteKeyword:(id)sender;
+- (IBAction)makeDefault:(id)sender;
+
+@end
+
+@interface KeywordEditorCocoaController (TestingAPI)
+
+// Instances of this class are managed, use +showKeywordEditor:.
+- (id)initWithProfile:(Profile*)profile;
+
+// Returns a reference to the shared instance for the given profile,
+// or nil if there is none.
++ (KeywordEditorCocoaController*)sharedInstanceForProfile:(Profile*)profile;
+
+// Converts a row index in our table view (which has group header rows) into
+// one in the |controller_|'s model, which does not have them.
+- (int)indexInModelForRow:(NSUInteger)row;
+
+@end
diff --git a/chrome/browser/ui/cocoa/keyword_editor_cocoa_controller.mm b/chrome/browser/ui/cocoa/keyword_editor_cocoa_controller.mm
new file mode 100644
index 0000000..109fcd9
--- /dev/null
+++ b/chrome/browser/ui/cocoa/keyword_editor_cocoa_controller.mm
@@ -0,0 +1,422 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/keyword_editor_cocoa_controller.h"
+
+#import "base/mac_util.h"
+#include "base/singleton.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/search_engines/template_url_model.h"
+#include "chrome/browser/search_engines/template_url_table_model.h"
+#import "chrome/browser/ui/cocoa/edit_search_engine_cocoa_controller.h"
+#import "chrome/browser/ui/cocoa/window_size_autosaver.h"
+#include "chrome/common/pref_names.h"
+#include "grit/generated_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+namespace {
+
+const CGFloat kButtonBarHeight = 35.0;
+
+} // namespace
+
+@interface KeywordEditorCocoaController (Private)
+- (void)adjustEditingButtons;
+- (void)editKeyword:(id)sender;
+- (int)indexInModelForRow:(NSUInteger)row;
+@end
+
+// KeywordEditorModelObserver -------------------------------------------------
+
+KeywordEditorModelObserver::KeywordEditorModelObserver(
+ KeywordEditorCocoaController* controller)
+ : controller_(controller),
+ icon_cache_(this) {
+}
+
+KeywordEditorModelObserver::~KeywordEditorModelObserver() {
+}
+
+// Notification that the template url model has changed in some way.
+void KeywordEditorModelObserver::OnTemplateURLModelChanged() {
+ [controller_ modelChanged];
+}
+
+void KeywordEditorModelObserver::OnEditedKeyword(
+ const TemplateURL* template_url,
+ const string16& title,
+ const string16& keyword,
+ const std::string& url) {
+ KeywordEditorController* controller = [controller_ controller];
+ if (template_url) {
+ controller->ModifyTemplateURL(template_url, title, keyword, url);
+ } else {
+ controller->AddTemplateURL(title, keyword, url);
+ }
+}
+
+void KeywordEditorModelObserver::OnModelChanged() {
+ icon_cache_.OnModelChanged();
+ [controller_ modelChanged];
+}
+
+void KeywordEditorModelObserver::OnItemsChanged(int start, int length) {
+ icon_cache_.OnItemsChanged(start, length);
+ [controller_ modelChanged];
+}
+
+void KeywordEditorModelObserver::OnItemsAdded(int start, int length) {
+ icon_cache_.OnItemsAdded(start, length);
+ [controller_ modelChanged];
+}
+
+void KeywordEditorModelObserver::OnItemsRemoved(int start, int length) {
+ icon_cache_.OnItemsRemoved(start, length);
+ [controller_ modelChanged];
+}
+
+int KeywordEditorModelObserver::RowCount() const {
+ return [controller_ controller]->table_model()->RowCount();
+}
+
+SkBitmap KeywordEditorModelObserver::GetIcon(int row) const {
+ return [controller_ controller]->table_model()->GetIcon(row);
+}
+
+NSImage* KeywordEditorModelObserver::GetImageForRow(int row) {
+ return icon_cache_.GetImageForRow(row);
+}
+
+// KeywordEditorCocoaController -----------------------------------------------
+
+namespace {
+
+typedef std::map<Profile*,KeywordEditorCocoaController*> ProfileControllerMap;
+
+} // namespace
+
+@implementation KeywordEditorCocoaController
+
++ (KeywordEditorCocoaController*)sharedInstanceForProfile:(Profile*)profile {
+ ProfileControllerMap* map = Singleton<ProfileControllerMap>::get();
+ DCHECK(map != NULL);
+ ProfileControllerMap::iterator it = map->find(profile);
+ if (it != map->end()) {
+ return it->second;
+ }
+ return nil;
+}
+
+// TODO(shess): The Windows code watches a single global window which
+// is not distinguished by profile. This code could distinguish by
+// profile by checking the controller's class and profile.
++ (void)showKeywordEditor:(Profile*)profile {
+ // http://crbug.com/23359 describes a case where this panel is
+ // opened from an incognito window, which can leave the panel
+ // holding onto a stale profile. Since the same panel is used
+ // either way, arrange to use the original profile instead.
+ profile = profile->GetOriginalProfile();
+
+ ProfileControllerMap* map = Singleton<ProfileControllerMap>::get();
+ DCHECK(map != NULL);
+ ProfileControllerMap::iterator it = map->find(profile);
+ if (it == map->end()) {
+ // Since we don't currently support multiple profiles, this class
+ // has not been tested against them, so document that assumption.
+ DCHECK_EQ(map->size(), 0U);
+
+ KeywordEditorCocoaController* controller =
+ [[self alloc] initWithProfile:profile];
+ it = map->insert(std::make_pair(profile, controller)).first;
+ }
+
+ [it->second showWindow:nil];
+}
+
+- (id)initWithProfile:(Profile*)profile {
+ DCHECK(profile);
+ NSString* nibpath = [mac_util::MainAppBundle()
+ pathForResource:@"KeywordEditor"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ profile_ = profile;
+ controller_.reset(new KeywordEditorController(profile_));
+ observer_.reset(new KeywordEditorModelObserver(self));
+ controller_->table_model()->SetObserver(observer_.get());
+ controller_->url_model()->AddObserver(observer_.get());
+ groupCell_.reset([[NSTextFieldCell alloc] init]);
+
+ if (g_browser_process && g_browser_process->local_state()) {
+ sizeSaver_.reset([[WindowSizeAutosaver alloc]
+ initWithWindow:[self window]
+ prefService:g_browser_process->local_state()
+ path:prefs::kKeywordEditorWindowPlacement]);
+ }
+ }
+ return self;
+}
+
+- (void)dealloc {
+ controller_->table_model()->SetObserver(NULL);
+ controller_->url_model()->RemoveObserver(observer_.get());
+ [tableView_ setDataSource:nil];
+ observer_.reset();
+ [super dealloc];
+}
+
+- (void)awakeFromNib {
+ // Make sure the button fits its label, but keep it the same height as the
+ // other two buttons.
+ [GTMUILocalizerAndLayoutTweaker sizeToFitView:makeDefaultButton_];
+ NSSize size = [makeDefaultButton_ frame].size;
+ size.height = NSHeight([addButton_ frame]);
+ [makeDefaultButton_ setFrameSize:size];
+
+ [[self window] setAutorecalculatesContentBorderThickness:NO
+ forEdge:NSMinYEdge];
+ [[self window] setContentBorderThickness:kButtonBarHeight
+ forEdge:NSMinYEdge];
+
+ [self adjustEditingButtons];
+ [tableView_ setDoubleAction:@selector(editKeyword:)];
+ [tableView_ setTarget:self];
+}
+
+// When the window closes, clean ourselves up.
+- (void)windowWillClose:(NSNotification*)notif {
+ [self autorelease];
+
+ ProfileControllerMap* map = Singleton<ProfileControllerMap>::get();
+ ProfileControllerMap::iterator it = map->find(profile_);
+ // It should not be possible for this to be missing.
+ // TODO(shess): Except that the unit test reaches in directly.
+ // Consider circling around and refactoring that.
+ //DCHECK(it != map->end());
+ if (it != map->end()) {
+ map->erase(it);
+ }
+}
+
+- (void)modelChanged {
+ [tableView_ reloadData];
+ [self adjustEditingButtons];
+}
+
+- (KeywordEditorController*)controller {
+ return controller_.get();
+}
+
+- (void)sheetDidEnd:(NSWindow*)sheet
+ returnCode:(NSInteger)code
+ context:(void*)context {
+ [sheet orderOut:self];
+}
+
+- (IBAction)addKeyword:(id)sender {
+ // The controller will release itself when the window closes.
+ EditSearchEngineCocoaController* editor =
+ [[EditSearchEngineCocoaController alloc] initWithProfile:profile_
+ delegate:observer_.get()
+ templateURL:NULL];
+ [NSApp beginSheet:[editor window]
+ modalForWindow:[self window]
+ modalDelegate:self
+ didEndSelector:@selector(sheetDidEnd:returnCode:context:)
+ contextInfo:NULL];
+}
+
+- (void)editKeyword:(id)sender {
+ const NSInteger clickedRow = [tableView_ clickedRow];
+ if (clickedRow < 0 || [self tableView:tableView_ isGroupRow:clickedRow])
+ return;
+ const TemplateURL* url = controller_->GetTemplateURL(
+ [self indexInModelForRow:clickedRow]);
+ // The controller will release itself when the window closes.
+ EditSearchEngineCocoaController* editor =
+ [[EditSearchEngineCocoaController alloc] initWithProfile:profile_
+ delegate:observer_.get()
+ templateURL:url];
+ [NSApp beginSheet:[editor window]
+ modalForWindow:[self window]
+ modalDelegate:self
+ didEndSelector:@selector(sheetDidEnd:returnCode:context:)
+ contextInfo:NULL];
+}
+
+- (IBAction)deleteKeyword:(id)sender {
+ NSIndexSet* selection = [tableView_ selectedRowIndexes];
+ DCHECK_GT([selection count], 0U);
+ NSUInteger index = [selection lastIndex];
+ while (index != NSNotFound) {
+ controller_->RemoveTemplateURL([self indexInModelForRow:index]);
+ index = [selection indexLessThanIndex:index];
+ }
+}
+
+- (IBAction)makeDefault:(id)sender {
+ NSIndexSet* selection = [tableView_ selectedRowIndexes];
+ DCHECK_EQ([selection count], 1U);
+ int row = [self indexInModelForRow:[selection firstIndex]];
+ controller_->MakeDefaultTemplateURL(row);
+}
+
+// Called when the user hits the escape key. Closes the window.
+- (void)cancel:(id)sender {
+ [[self window] performClose:self];
+}
+
+// Table View Data Source -----------------------------------------------------
+
+- (NSInteger)numberOfRowsInTableView:(NSTableView*)table {
+ int rowCount = controller_->table_model()->RowCount();
+ int numGroups = controller_->table_model()->GetGroups().size();
+ if ([self tableView:table isGroupRow:rowCount + numGroups - 1]) {
+ // Don't show a group header with no rows underneath it.
+ --numGroups;
+ }
+ return rowCount + numGroups;
+}
+
+- (id)tableView:(NSTableView*)tv
+ objectValueForTableColumn:(NSTableColumn*)tableColumn
+ row:(NSInteger)row {
+ if ([self tableView:tv isGroupRow:row]) {
+ DCHECK(!tableColumn);
+ TableModel::Groups groups = controller_->table_model()->GetGroups();
+ if (row == 0) {
+ return base::SysWideToNSString(groups[0].title);
+ } else {
+ return base::SysWideToNSString(groups[1].title);
+ }
+ }
+
+ NSString* identifier = [tableColumn identifier];
+ if ([identifier isEqualToString:@"name"]) {
+ // The name column is an NSButtonCell so we can have text and image in the
+ // same cell. As such, the "object value" for a button cell is either on
+ // or off, so we always return off so we don't act like a button.
+ return [NSNumber numberWithInt:NSOffState];
+ }
+ if ([identifier isEqualToString:@"keyword"]) {
+ // The keyword object value is a normal string.
+ int index = [self indexInModelForRow:row];
+ int columnID = IDS_SEARCH_ENGINES_EDITOR_KEYWORD_COLUMN;
+ std::wstring text = controller_->table_model()->GetText(index, columnID);
+ return base::SysWideToNSString(text);
+ }
+
+ // And we shouldn't have any other columns...
+ NOTREACHED();
+ return nil;
+}
+
+// Table View Delegate --------------------------------------------------------
+
+// When the selection in the table view changes, we need to adjust buttons.
+- (void)tableViewSelectionDidChange:(NSNotification*)aNotification {
+ [self adjustEditingButtons];
+}
+
+// Disallow selection of the group header rows.
+- (BOOL)tableView:(NSTableView*)table shouldSelectRow:(NSInteger)row {
+ return ![self tableView:table isGroupRow:row];
+}
+
+- (BOOL)tableView:(NSTableView*)table isGroupRow:(NSInteger)row {
+ int otherGroupRow =
+ controller_->table_model()->last_search_engine_index() + 1;
+ return (row == 0 || row == otherGroupRow);
+}
+
+- (NSCell*)tableView:(NSTableView*)tableView
+ dataCellForTableColumn:(NSTableColumn*)tableColumn
+ row:(NSInteger)row {
+ static const CGFloat kCellFontSize = 12.0;
+
+ // Check to see if we are a grouped row.
+ if ([self tableView:tableView isGroupRow:row]) {
+ DCHECK(!tableColumn); // This would violate the group row contract.
+ return groupCell_.get();
+ }
+
+ NSCell* cell = [tableColumn dataCellForRow:row];
+ int offsetRow = [self indexInModelForRow:row];
+
+ // Set the favicon and title for the search engine in the name column.
+ if ([[tableColumn identifier] isEqualToString:@"name"]) {
+ DCHECK([cell isKindOfClass:[NSButtonCell class]]);
+ NSButtonCell* buttonCell = static_cast<NSButtonCell*>(cell);
+ std::wstring title = controller_->table_model()->GetText(offsetRow,
+ IDS_SEARCH_ENGINES_EDITOR_DESCRIPTION_COLUMN);
+ [buttonCell setTitle:base::SysWideToNSString(title)];
+ [buttonCell setImage:observer_->GetImageForRow(offsetRow)];
+ [buttonCell setRefusesFirstResponder:YES]; // Don't push in like a button.
+ [buttonCell setHighlightsBy:NSNoCellMask];
+ }
+
+ // The default search engine should be in bold font.
+ const TemplateURL* defaultEngine =
+ controller_->url_model()->GetDefaultSearchProvider();
+ int rowIndex = controller_->table_model()->IndexOfTemplateURL(defaultEngine);
+ if (rowIndex == offsetRow) {
+ [cell setFont:[NSFont boldSystemFontOfSize:kCellFontSize]];
+ } else {
+ [cell setFont:[NSFont systemFontOfSize:kCellFontSize]];
+ }
+ return cell;
+}
+
+// Private --------------------------------------------------------------------
+
+// This function appropriately sets the enabled states on the table's editing
+// buttons.
+- (void)adjustEditingButtons {
+ NSIndexSet* selection = [tableView_ selectedRowIndexes];
+ BOOL canRemove = ([selection count] > 0);
+ NSUInteger index = [selection firstIndex];
+
+ // Delete button.
+ while (canRemove && index != NSNotFound) {
+ int modelIndex = [self indexInModelForRow:index];
+ const TemplateURL& url =
+ controller_->table_model()->GetTemplateURL(modelIndex);
+ if (!controller_->CanRemove(&url))
+ canRemove = NO;
+ index = [selection indexGreaterThanIndex:index];
+ }
+ [removeButton_ setEnabled:canRemove];
+
+ // Make default button.
+ if ([selection count] != 1) {
+ [makeDefaultButton_ setEnabled:NO];
+ } else {
+ int row = [self indexInModelForRow:[selection firstIndex]];
+ const TemplateURL& url =
+ controller_->table_model()->GetTemplateURL(row);
+ [makeDefaultButton_ setEnabled:controller_->CanMakeDefault(&url)];
+ }
+}
+
+// This converts a row index in our table view to an index in the model by
+// computing the group offsets.
+- (int)indexInModelForRow:(NSUInteger)row {
+ DCHECK_GT(row, 0U);
+ unsigned otherGroupId =
+ controller_->table_model()->last_search_engine_index() + 1;
+ DCHECK_NE(row, otherGroupId);
+ if (row >= otherGroupId) {
+ return row - 2; // Other group.
+ } else {
+ return row - 1; // Default group.
+ }
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/keyword_editor_cocoa_controller_unittest.mm b/chrome/browser/ui/cocoa/keyword_editor_cocoa_controller_unittest.mm
new file mode 100644
index 0000000..eb5c264
--- /dev/null
+++ b/chrome/browser/ui/cocoa/keyword_editor_cocoa_controller_unittest.mm
@@ -0,0 +1,227 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/mac/scoped_nsautorelease_pool.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/search_engines/template_url.h"
+#include "chrome/browser/search_engines/template_url_model.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/keyword_editor_cocoa_controller.h"
+#include "chrome/test/testing_profile.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface FakeKeywordEditorController : KeywordEditorCocoaController {
+ @public
+ BOOL modelChanged_;
+}
+- (void)modelChanged;
+- (BOOL)hasModelChanged;
+- (KeywordEditorModelObserver*)observer;
+@end
+
+@implementation FakeKeywordEditorController
+
+- (void)modelChanged {
+ modelChanged_ = YES;
+}
+
+- (BOOL)hasModelChanged {
+ return modelChanged_;
+}
+
+- (KeywordEditorModelObserver*)observer {
+ return observer_.get();
+}
+
+- (NSTableView*)tableView {
+ return tableView_;
+}
+
+@end
+
+// TODO(rsesek): Figure out a good way to test this class (crbug.com/21640).
+
+namespace {
+
+class KeywordEditorCocoaControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ TestingProfile* profile =
+ static_cast<TestingProfile*>(browser_helper_.profile());
+ profile->CreateTemplateURLModel();
+
+ controller_ =
+ [[FakeKeywordEditorController alloc] initWithProfile:profile];
+ }
+
+ virtual void TearDown() {
+ // Force the window to load so we hit |-awakeFromNib| to register as the
+ // window's delegate so that the controller can clean itself up in
+ // |-windowWillClose:|.
+ ASSERT_TRUE([controller_ window]);
+
+ [controller_ close];
+ CocoaTest::TearDown();
+ }
+
+ // Helper to count the keyword editors.
+ NSUInteger CountKeywordEditors() {
+ base::mac::ScopedNSAutoreleasePool pool;
+ NSUInteger count = 0;
+ for (NSWindow* window in [NSApp windows]) {
+ id controller = [window windowController];
+ if ([controller isKindOfClass:[KeywordEditorCocoaController class]]) {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ BrowserTestHelper browser_helper_;
+ FakeKeywordEditorController* controller_;
+};
+
+TEST_F(KeywordEditorCocoaControllerTest, TestModelChanged) {
+ EXPECT_FALSE([controller_ hasModelChanged]);
+ KeywordEditorModelObserver* observer = [controller_ observer];
+ observer->OnTemplateURLModelChanged();
+ EXPECT_TRUE([controller_ hasModelChanged]);
+}
+
+// Test that +showKeywordEditor brings up the existing editor and
+// creates one if needed.
+TEST_F(KeywordEditorCocoaControllerTest, ShowKeywordEditor) {
+ // No outstanding editors.
+ Profile* profile(browser_helper_.profile());
+ KeywordEditorCocoaController* sharedInstance =
+ [KeywordEditorCocoaController sharedInstanceForProfile:profile];
+ EXPECT_TRUE(nil == sharedInstance);
+ EXPECT_EQ(CountKeywordEditors(), 0U);
+
+ const NSUInteger initial_window_count([[NSApp windows] count]);
+
+ // The window unwinds using -autorelease, so we need to introduce an
+ // autorelease pool to really test whether it went away or not.
+ {
+ base::mac::ScopedNSAutoreleasePool pool;
+
+ // +showKeywordEditor: creates a new controller.
+ [KeywordEditorCocoaController showKeywordEditor:profile];
+ sharedInstance =
+ [KeywordEditorCocoaController sharedInstanceForProfile:profile];
+ EXPECT_TRUE(sharedInstance);
+ EXPECT_EQ(CountKeywordEditors(), 1U);
+
+ // Another call doesn't create another controller.
+ [KeywordEditorCocoaController showKeywordEditor:profile];
+ EXPECT_TRUE(sharedInstance ==
+ [KeywordEditorCocoaController sharedInstanceForProfile:profile]);
+ EXPECT_EQ(CountKeywordEditors(), 1U);
+
+ [sharedInstance close];
+ }
+
+ // No outstanding editors.
+ sharedInstance =
+ [KeywordEditorCocoaController sharedInstanceForProfile:profile];
+ EXPECT_TRUE(nil == sharedInstance);
+ EXPECT_EQ(CountKeywordEditors(), 0U);
+
+ // Windows we created should be gone.
+ EXPECT_EQ([[NSApp windows] count], initial_window_count);
+
+ // Get a new editor, should be different from the previous one.
+ [KeywordEditorCocoaController showKeywordEditor:profile];
+ KeywordEditorCocoaController* newSharedInstance =
+ [KeywordEditorCocoaController sharedInstanceForProfile:profile];
+ EXPECT_TRUE(sharedInstance != newSharedInstance);
+ EXPECT_EQ(CountKeywordEditors(), 1U);
+ [newSharedInstance close];
+}
+
+TEST_F(KeywordEditorCocoaControllerTest, IndexInModelForRowMixed) {
+ [controller_ window]; // Force |-awakeFromNib|.
+ TemplateURLModel* template_model = [controller_ controller]->url_model();
+
+ // Add a default engine.
+ TemplateURL* t_url = new TemplateURL();
+ t_url->SetURL("http://test1/{searchTerms}", 0, 0);
+ t_url->set_keyword(L"test1");
+ t_url->set_short_name(L"Test1");
+ t_url->set_show_in_default_list(true);
+ template_model->Add(t_url);
+
+ // Add a non-default engine.
+ t_url = new TemplateURL();
+ t_url->SetURL("http://test2/{searchTerms}", 0, 0);
+ t_url->set_keyword(L"test2");
+ t_url->set_short_name(L"Test2");
+ t_url->set_show_in_default_list(false);
+ template_model->Add(t_url);
+
+ // Two headers with a single row underneath each.
+ NSTableView* table = [controller_ tableView];
+ [table reloadData];
+ ASSERT_EQ(4, [[controller_ tableView] numberOfRows]);
+
+ // Index 0 is the group header, index 1 should be the first engine.
+ ASSERT_EQ(0, [controller_ indexInModelForRow:1]);
+
+ // Index 2 should be the group header, so index 3 should be the non-default
+ // engine.
+ ASSERT_EQ(1, [controller_ indexInModelForRow:3]);
+
+ ASSERT_TRUE([controller_ tableView:table isGroupRow:0]);
+ ASSERT_FALSE([controller_ tableView:table isGroupRow:1]);
+ ASSERT_TRUE([controller_ tableView:table isGroupRow:2]);
+ ASSERT_FALSE([controller_ tableView:table isGroupRow:3]);
+
+ ASSERT_FALSE([controller_ tableView:table shouldSelectRow:0]);
+ ASSERT_TRUE([controller_ tableView:table shouldSelectRow:1]);
+ ASSERT_FALSE([controller_ tableView:table shouldSelectRow:2]);
+ ASSERT_TRUE([controller_ tableView:table shouldSelectRow:3]);
+}
+
+TEST_F(KeywordEditorCocoaControllerTest, IndexInModelForDefault) {
+ [controller_ window]; // Force |-awakeFromNib|.
+ TemplateURLModel* template_model = [controller_ controller]->url_model();
+
+ // Add 2 default engines.
+ TemplateURL* t_url = new TemplateURL();
+ t_url->SetURL("http://test1/{searchTerms}", 0, 0);
+ t_url->set_keyword(L"test1");
+ t_url->set_short_name(L"Test1");
+ t_url->set_show_in_default_list(true);
+ template_model->Add(t_url);
+
+ t_url = new TemplateURL();
+ t_url->SetURL("http://test2/{searchTerms}", 0, 0);
+ t_url->set_keyword(L"test2");
+ t_url->set_short_name(L"Test2");
+ t_url->set_show_in_default_list(true);
+ template_model->Add(t_url);
+
+ // One header and two rows.
+ NSTableView* table = [controller_ tableView];
+ [table reloadData];
+ ASSERT_EQ(3, [[controller_ tableView] numberOfRows]);
+
+ // Index 0 is the group header, index 1 should be the first engine.
+ ASSERT_EQ(0, [controller_ indexInModelForRow:1]);
+ ASSERT_EQ(1, [controller_ indexInModelForRow:2]);
+
+ ASSERT_TRUE([controller_ tableView:table isGroupRow:0]);
+ ASSERT_FALSE([controller_ tableView:table isGroupRow:1]);
+ ASSERT_FALSE([controller_ tableView:table isGroupRow:2]);
+
+ ASSERT_FALSE([controller_ tableView:table shouldSelectRow:0]);
+ ASSERT_TRUE([controller_ tableView:table shouldSelectRow:1]);
+ ASSERT_TRUE([controller_ tableView:table shouldSelectRow:2]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/l10n_util.h b/chrome/browser/ui/cocoa/l10n_util.h
new file mode 100644
index 0000000..bb26327
--- /dev/null
+++ b/chrome/browser/ui/cocoa/l10n_util.h
@@ -0,0 +1,32 @@
+// 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/string16.h"
+
+namespace cocoa_l10n_util {
+
+// Compare function for -[NSArray sortedArrayUsingFunction:context:] that
+// sorts the views in Y order bottom up. |context| is ignored.
+NSInteger CompareFrameY(id view1, id view2, void* context);
+
+// Helper for tweaking views. If view is a:
+// checkbox, radio group or label: it gets a forced wrap at current size
+// editable field: left as is
+// anything else: do +[GTMUILocalizerAndLayoutTweaker sizeToFitView:]
+NSSize WrapOrSizeToFit(NSView* view);
+
+// Walks views in top-down order, wraps each to their current width, and moves
+// the latter ones down to prevent overlaps. Returns the vertical delta in view
+// coordinates.
+CGFloat VerticallyReflowGroup(NSArray* views);
+
+// Like |ReplaceStringPlaceholders(const string16&, const string16&, size_t*)|,
+// but for a NSString formatString.
+NSString* ReplaceNSStringPlaceholders(NSString* formatString,
+ const string16& a,
+ size_t* offset);
+
+} // namespace cocoa_l10n_util
diff --git a/chrome/browser/ui/cocoa/l10n_util.mm b/chrome/browser/ui/cocoa/l10n_util.mm
new file mode 100644
index 0000000..5ebb8c7
--- /dev/null
+++ b/chrome/browser/ui/cocoa/l10n_util.mm
@@ -0,0 +1,78 @@
+// 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/ui/cocoa/l10n_util.h"
+
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+namespace cocoa_l10n_util {
+
+NSInteger CompareFrameY(id view1, id view2, void* context) {
+ CGFloat y1 = NSMinY([view1 frame]);
+ CGFloat y2 = NSMinY([view2 frame]);
+ if (y1 < y2)
+ return NSOrderedAscending;
+ else if (y1 > y2)
+ return NSOrderedDescending;
+ else
+ return NSOrderedSame;
+}
+
+NSSize WrapOrSizeToFit(NSView* view) {
+ if ([view isKindOfClass:[NSTextField class]]) {
+ NSTextField* textField = static_cast<NSTextField*>(view);
+ if ([textField isEditable])
+ return NSZeroSize;
+ CGFloat heightChange =
+ [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:textField];
+ return NSMakeSize(0.0, heightChange);
+ }
+ if ([view isKindOfClass:[NSMatrix class]]) {
+ NSMatrix* radioGroup = static_cast<NSMatrix*>(view);
+ [GTMUILocalizerAndLayoutTweaker wrapRadioGroupForWidth:radioGroup];
+ return [GTMUILocalizerAndLayoutTweaker sizeToFitView:view];
+ }
+ if ([view isKindOfClass:[NSButton class]]) {
+ NSButton* button = static_cast<NSButton*>(view);
+ NSButtonCell* buttonCell = [button cell];
+ // Decide it's a checkbox via showsStateBy and highlightsBy.
+ if (([buttonCell showsStateBy] == NSCellState) &&
+ ([buttonCell highlightsBy] == NSCellState)) {
+ [GTMUILocalizerAndLayoutTweaker wrapButtonTitleForWidth:button];
+ return [GTMUILocalizerAndLayoutTweaker sizeToFitView:view];
+ }
+ }
+ return [GTMUILocalizerAndLayoutTweaker sizeToFitView:view];
+}
+
+CGFloat VerticallyReflowGroup(NSArray* views) {
+ views = [views sortedArrayUsingFunction:CompareFrameY
+ context:NULL];
+ CGFloat localVerticalShift = 0;
+ for (NSInteger index = [views count] - 1; index >= 0; --index) {
+ NSView* view = [views objectAtIndex:index];
+
+ NSSize delta = WrapOrSizeToFit(view);
+ localVerticalShift += delta.height;
+ if (localVerticalShift) {
+ NSPoint origin = [view frame].origin;
+ origin.y -= localVerticalShift;
+ [view setFrameOrigin:origin];
+ }
+ }
+ return localVerticalShift;
+}
+
+NSString* ReplaceNSStringPlaceholders(NSString* formatString,
+ const string16& a,
+ size_t* offset) {
+ return base::SysUTF16ToNSString(
+ ReplaceStringPlaceholders(base::SysNSStringToUTF16(formatString),
+ a,
+ offset));
+}
+
+} // namespace cocoa_l10n_util
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h
new file mode 100644
index 0000000..e731c2c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h
@@ -0,0 +1,144 @@
+// 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_UI_COCOA_AUTOCOMPLETE_TEXT_FIELD_H_
+#define CHROME_BROWSER_UI_COCOA_AUTOCOMPLETE_TEXT_FIELD_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/styled_text_field.h"
+#import "chrome/browser/ui/cocoa/url_drop_target.h"
+
+@class AutocompleteTextFieldCell;
+
+// AutocompleteTextField intercepts UI actions for forwarding to
+// AutocompleteEditViewMac (*), and provides a custom look. It works
+// together with AutocompleteTextFieldEditor (mostly for intercepting
+// user actions) and AutocompleteTextFieldCell (mostly for custom
+// drawing).
+//
+// For historical reasons, chrome/browser/autocomplete is the core
+// implementation of the Omnibox. Chrome code seems to vary between
+// autocomplete and Omnibox in describing this.
+//
+// (*) AutocompleteEditViewMac is a view in the MVC sense for the
+// Chrome internals, though it's really more of a mish-mash of model,
+// view, and controller.
+
+// Provides a hook so that we can call directly down to
+// AutocompleteEditViewMac rather than traversing the delegate chain.
+class AutocompleteTextFieldObserver {
+ public:
+ // Called before changing the selected range of the field.
+ virtual NSRange SelectionRangeForProposedRange(NSRange proposed_range) = 0;
+
+ // Called when the control-key state changes while the field is
+ // first responder.
+ virtual void OnControlKeyChanged(bool pressed) = 0;
+
+ // Called when the user pastes into the field.
+ virtual void OnPaste() = 0;
+
+ // Return |true| if there is a selection to copy.
+ virtual bool CanCopy() = 0;
+
+ // Clears the |pboard| and adds the field's current selection.
+ // Called when the user does a copy or drag.
+ virtual void CopyToPasteboard(NSPasteboard* pboard) = 0;
+
+ // Returns true if the current clipboard text supports paste and go
+ // (or paste and search).
+ virtual bool CanPasteAndGo() = 0;
+
+ // Returns the appropriate "Paste and Go" or "Paste and Search"
+ // context menu string, depending on what is currently in the
+ // clipboard. Must not be called unless CanPasteAndGo() returns
+ // true.
+ virtual int GetPasteActionStringId() = 0;
+
+ // Called when the user initiates a "paste and go" or "paste and
+ // search" into the field.
+ virtual void OnPasteAndGo() = 0;
+
+ // Called when the field's frame changes.
+ virtual void OnFrameChanged() = 0;
+
+ // Called when the popup is no longer appropriate, such as when the
+ // field's window loses focus or a page action is clicked.
+ virtual void ClosePopup() = 0;
+
+ // Called when the user begins editing the field, for every edit,
+ // and when the user is done editing the field.
+ virtual void OnDidBeginEditing() = 0;
+ virtual void OnDidChange() = 0;
+ virtual void OnDidEndEditing() = 0;
+
+ // NSResponder translates certain keyboard actions into selectors
+ // passed to -doCommandBySelector:. The selector is forwarded here,
+ // return true if |cmd| is handled, false if the caller should
+ // handle it.
+ // TODO(shess): For now, I think having the code which makes these
+ // decisions closer to the other autocomplete code is worthwhile,
+ // since it calls a wide variety of methods which otherwise aren't
+ // clearly relevent to expose here. But consider pulling more of
+ // the AutocompleteEditViewMac calls up to here.
+ virtual bool OnDoCommandBySelector(SEL cmd) = 0;
+
+ // Called whenever the autocomplete text field gets focused.
+ virtual void OnSetFocus(bool control_down) = 0;
+
+ // Called whenever the autocomplete text field is losing focus.
+ virtual void OnKillFocus() = 0;
+
+ protected:
+ virtual ~AutocompleteTextFieldObserver() {}
+};
+
+@interface AutocompleteTextField : StyledTextField<NSTextViewDelegate,
+ URLDropTarget> {
+ @private
+ // Undo manager for this text field. We use a specific instance rather than
+ // the standard undo manager in order to let us clear the undo stack at will.
+ scoped_nsobject<NSUndoManager> undoManager_;
+
+ AutocompleteTextFieldObserver* observer_; // weak, owned by location bar.
+
+ // Handles being a drag-and-drop target.
+ scoped_nsobject<URLDropTargetHandler> dropHandler_;
+
+ // Holds current tooltip strings, to keep them from being dealloced.
+ scoped_nsobject<NSMutableArray> currentToolTips_;
+}
+
+@property (nonatomic) AutocompleteTextFieldObserver* observer;
+
+// Convenience method to return the cell, casted appropriately.
+- (AutocompleteTextFieldCell*)cell;
+
+// Superclass aborts editing before changing the string, which causes
+// problems for undo. This version modifies the field editor's
+// contents if the control is already being edited.
+- (void)setAttributedStringValue:(NSAttributedString*)aString;
+
+// Clears the undo chain for this text field.
+- (void)clearUndoChain;
+
+// Updates cursor and tooltip rects depending on the contents of the text field
+// e.g. the security icon should have a default pointer shown on hover instead
+// of an I-beam.
+- (void)updateCursorAndToolTipRects;
+
+// Return the appropriate menu for any decoration under |event|.
+- (NSMenu*)decorationMenuForEvent:(NSEvent*)event;
+
+// Retains |tooltip| (in |currentToolTips_|) and adds this tooltip
+// via -[NSView addToolTipRect:owner:userData:].
+- (void)addToolTip:(NSString*)tooltip forRect:(NSRect)aRect;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_AUTOCOMPLETE_TEXT_FIELD_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.mm b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.mm
new file mode 100644
index 0000000..33c34cf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.mm
@@ -0,0 +1,385 @@
+// 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/ui/cocoa/location_bar/autocomplete_text_field.h"
+
+#include "base/logging.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
+#import "chrome/browser/ui/cocoa/toolbar_controller.h"
+#import "chrome/browser/ui/cocoa/url_drop_target.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+
+@implementation AutocompleteTextField
+
+@synthesize observer = observer_;
+
++ (Class)cellClass {
+ return [AutocompleteTextFieldCell class];
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+- (void)awakeFromNib {
+ DCHECK([[self cell] isKindOfClass:[AutocompleteTextFieldCell class]]);
+ [[self cell] setTruncatesLastVisibleLine:YES];
+ [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail];
+ currentToolTips_.reset([[NSMutableArray alloc] init]);
+}
+
+- (void)flagsChanged:(NSEvent*)theEvent {
+ if (observer_) {
+ const bool controlFlag = ([theEvent modifierFlags]&NSControlKeyMask) != 0;
+ observer_->OnControlKeyChanged(controlFlag);
+ }
+}
+
+- (AutocompleteTextFieldCell*)cell {
+ NSCell* cell = [super cell];
+ if (!cell)
+ return nil;
+
+ DCHECK([cell isKindOfClass:[AutocompleteTextFieldCell class]]);
+ return static_cast<AutocompleteTextFieldCell*>(cell);
+}
+
+// Reroute events for the decoration area to the field editor. This
+// will cause the cursor to be moved as close to the edge where the
+// event was seen as possible.
+//
+// The reason for this code's existence is subtle. NSTextField
+// implements text selection and editing in terms of a "field editor".
+// This is an NSTextView which is installed as a subview of the
+// control when the field becomes first responder. When the field
+// editor is installed, it will get -mouseDown: events and handle
+// them, rather than the text field - EXCEPT for the event which
+// caused the change in first responder, or events which fall in the
+// decorations outside the field editor's area. In that case, the
+// default NSTextField code will setup the field editor all over
+// again, which has the side effect of doing "select all" on the text.
+// This effect can be observed with a normal NSTextField if you click
+// in the narrow border area, and is only really a problem because in
+// our case the focus ring surrounds decorations which look clickable.
+//
+// When the user first clicks on the field, after installing the field
+// editor the default NSTextField code detects if the hit is in the
+// field editor area, and if so sets the selection to {0,0} to clear
+// the selection before forwarding the event to the field editor for
+// processing (it will set the cursor position). This also starts the
+// click-drag selection machinery.
+//
+// This code does the same thing for cases where the click was in the
+// decoration area. This allows the user to click-drag starting from
+// a decoration area and get the expected selection behaviour,
+// likewise for multiple clicks in those areas.
+- (void)mouseDown:(NSEvent*)theEvent {
+ // Close the popup before processing the event. This prevents the
+ // popup from being visible while a right-click context menu or
+ // page-action menu is visible. Also, it matches other platforms.
+ if (observer_)
+ observer_->ClosePopup();
+
+ // If the click was a Control-click, bring up the context menu.
+ // |NSTextField| handles these cases inconsistently if the field is
+ // not already first responder.
+ if (([theEvent modifierFlags] & NSControlKeyMask) != 0) {
+ NSText* editor = [self currentEditor];
+ NSMenu* menu = [editor menuForEvent:theEvent];
+ [NSMenu popUpContextMenu:menu withEvent:theEvent forView:editor];
+ return;
+ }
+
+ const NSPoint location =
+ [self convertPoint:[theEvent locationInWindow] fromView:nil];
+ const NSRect bounds([self bounds]);
+
+ AutocompleteTextFieldCell* cell = [self cell];
+ const NSRect textFrame([cell textFrameForFrame:bounds]);
+
+ // A version of the textFrame which extends across the field's
+ // entire width.
+
+ const NSRect fullFrame(NSMakeRect(bounds.origin.x, textFrame.origin.y,
+ bounds.size.width, textFrame.size.height));
+
+ // If the mouse is in the editing area, or above or below where the
+ // editing area would be if we didn't add decorations, forward to
+ // NSTextField -mouseDown: because it does the right thing. The
+ // above/below test is needed because NSTextView treats mouse events
+ // above/below as select-to-end-in-that-direction, which makes
+ // things janky.
+ BOOL flipped = [self isFlipped];
+ if (NSMouseInRect(location, textFrame, flipped) ||
+ !NSMouseInRect(location, fullFrame, flipped)) {
+ [super mouseDown:theEvent];
+
+ // After the event has been handled, if the current event is a
+ // mouse up and no selection was created (the mouse didn't move),
+ // select the entire field.
+ // NOTE(shess): This does not interfere with single-clicking to
+ // place caret after a selection is made. An NSTextField only has
+ // a selection when it has a field editor. The field editor is an
+ // NSText subview, which will receive the -mouseDown: in that
+ // case, and this code will never fire.
+ NSText* editor = [self currentEditor];
+ if (editor) {
+ NSEvent* currentEvent = [NSApp currentEvent];
+ if ([currentEvent type] == NSLeftMouseUp &&
+ ![editor selectedRange].length) {
+ [editor selectAll:nil];
+ }
+ }
+
+ return;
+ }
+
+ // Give the cell a chance to intercept clicks in page-actions and
+ // other decorative items.
+ if ([cell mouseDown:theEvent inRect:bounds ofView:self]) {
+ return;
+ }
+
+ NSText* editor = [self currentEditor];
+
+ // We should only be here if we accepted first-responder status and
+ // have a field editor. If one of these fires, it means some
+ // assumptions are being broken.
+ DCHECK(editor != nil);
+ DCHECK([editor isDescendantOf:self]);
+
+ // -becomeFirstResponder does a select-all, which we don't want
+ // because it can lead to a dragged-text situation. Clear the
+ // selection (any valid empty selection will do).
+ [editor setSelectedRange:NSMakeRange(0, 0)];
+
+ // If the event is to the right of the editing area, scroll the
+ // field editor to the end of the content so that the selection
+ // doesn't initiate from somewhere in the middle of the text.
+ if (location.x > NSMaxX(textFrame)) {
+ [editor scrollRangeToVisible:NSMakeRange([[self stringValue] length], 0)];
+ }
+
+ [editor mouseDown:theEvent];
+}
+
+// Overridden to pass OnFrameChanged() notifications to |observer_|.
+// Additionally, cursor and tooltip rects need to be updated.
+- (void)setFrame:(NSRect)frameRect {
+ [super setFrame:frameRect];
+ if (observer_) {
+ observer_->OnFrameChanged();
+ }
+ [self updateCursorAndToolTipRects];
+}
+
+// Due to theming, parts of the field are transparent.
+- (BOOL)isOpaque {
+ return NO;
+}
+
+- (void)setAttributedStringValue:(NSAttributedString*)aString {
+ AutocompleteTextFieldEditor* editor =
+ static_cast<AutocompleteTextFieldEditor*>([self currentEditor]);
+
+ if (!editor) {
+ [super setAttributedStringValue:aString];
+ } else {
+ // The type of the field editor must be AutocompleteTextFieldEditor,
+ // otherwise things won't work.
+ DCHECK([editor isKindOfClass:[AutocompleteTextFieldEditor class]]);
+
+ [editor setAttributedString:aString];
+ }
+}
+
+- (NSUndoManager*)undoManagerForTextView:(NSTextView*)textView {
+ if (!undoManager_.get())
+ undoManager_.reset([[NSUndoManager alloc] init]);
+ return undoManager_.get();
+}
+
+- (void)clearUndoChain {
+ [undoManager_ removeAllActions];
+}
+
+- (NSRange)textView:(NSTextView *)aTextView
+ willChangeSelectionFromCharacterRange:(NSRange)oldRange
+ toCharacterRange:(NSRange)newRange {
+ if (observer_)
+ return observer_->SelectionRangeForProposedRange(newRange);
+ return newRange;
+}
+
+- (void)addToolTip:(NSString*)tooltip forRect:(NSRect)aRect {
+ [currentToolTips_ addObject:tooltip];
+ [self addToolTipRect:aRect owner:tooltip userData:nil];
+}
+
+// TODO(shess): -resetFieldEditorFrameIfNeeded is the place where
+// changes to the cell layout should be flushed. LocationBarViewMac
+// and ToolbarController are calling this routine directly, and I
+// think they are probably wrong.
+// http://crbug.com/40053
+- (void)updateCursorAndToolTipRects {
+ // This will force |resetCursorRects| to be called, as it is not to be called
+ // directly.
+ [[self window] invalidateCursorRectsForView:self];
+
+ // |removeAllToolTips| only removes those set on the current NSView, not any
+ // subviews. Unless more tooltips are added to this view, this should suffice
+ // in place of managing a set of NSToolTipTag objects.
+ [self removeAllToolTips];
+
+ // Reload the decoration tooltips.
+ [currentToolTips_ removeAllObjects];
+ [[self cell] updateToolTipsInRect:[self bounds] ofView:self];
+}
+
+// NOTE(shess): http://crbug.com/19116 describes a weird bug which
+// happens when the user runs a Print panel on Leopard. After that,
+// spurious -controlTextDidBeginEditing notifications are sent when an
+// NSTextField is firstResponder, even though -currentEditor on that
+// field returns nil. That notification caused significant problems
+// in AutocompleteEditViewMac. -textDidBeginEditing: was NOT being
+// sent in those cases, so this approach doesn't have the problem.
+- (void)textDidBeginEditing:(NSNotification*)aNotification {
+ [super textDidBeginEditing:aNotification];
+ if (observer_) {
+ observer_->OnDidBeginEditing();
+ }
+}
+
+- (void)textDidEndEditing:(NSNotification *)aNotification {
+ [super textDidEndEditing:aNotification];
+ if (observer_) {
+ observer_->OnDidEndEditing();
+ }
+}
+
+// When the window resigns, make sure the autocomplete popup is no
+// longer visible, since the user's focus is elsewhere.
+- (void)windowDidResignKey:(NSNotification*)notification {
+ DCHECK_EQ([self window], [notification object]);
+ if (observer_)
+ observer_->ClosePopup();
+}
+
+- (void)viewWillMoveToWindow:(NSWindow*)newWindow {
+ if ([self window]) {
+ NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
+ [nc removeObserver:self
+ name:NSWindowDidResignKeyNotification
+ object:[self window]];
+ }
+}
+
+- (void)viewDidMoveToWindow {
+ if ([self window]) {
+ NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
+ [nc addObserver:self
+ selector:@selector(windowDidResignKey:)
+ name:NSWindowDidResignKeyNotification
+ object:[self window]];
+ // Only register for drops if not in a popup window. Lazily create the
+ // drop handler when the type of window is known.
+ BrowserWindowController* windowController =
+ [BrowserWindowController browserWindowControllerForView:self];
+ if ([windowController isNormalWindow])
+ dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]);
+ }
+}
+
+// NSTextField becomes first responder by installing a "field editor"
+// subview. Clicks outside the field editor (such as a decoration)
+// will attempt to make the field the first-responder again, which
+// causes a select-all, even if the decoration handles the click. If
+// the field editor is already in place, don't accept first responder
+// again. This allows the selection to be unmodified if the click is
+// handled by a decoration or context menu (|-mouseDown:| will still
+// change it if appropriate).
+- (BOOL)acceptsFirstResponder {
+ if ([self currentEditor]) {
+ DCHECK_EQ([self currentEditor], [[self window] firstResponder]);
+ return NO;
+ }
+ return [super acceptsFirstResponder];
+}
+
+// (Overridden from NSResponder)
+- (BOOL)becomeFirstResponder {
+ BOOL doAccept = [super becomeFirstResponder];
+ if (doAccept) {
+ [[BrowserWindowController browserWindowControllerForView:self]
+ lockBarVisibilityForOwner:self withAnimation:YES delay:NO];
+
+ // Tells the observer that we get the focus.
+ // But we can't call observer_->OnKillFocus() in resignFirstResponder:,
+ // because the first responder will be immediately set to the field editor
+ // when calling [super becomeFirstResponder], thus we won't receive
+ // resignFirstResponder: anymore when losing focus.
+ if (observer_) {
+ NSEvent* theEvent = [NSApp currentEvent];
+ const bool controlDown = ([theEvent modifierFlags]&NSControlKeyMask) != 0;
+ observer_->OnSetFocus(controlDown);
+ }
+ }
+ return doAccept;
+}
+
+// (Overridden from NSResponder)
+- (BOOL)resignFirstResponder {
+ BOOL doResign = [super resignFirstResponder];
+ if (doResign) {
+ [[BrowserWindowController browserWindowControllerForView:self]
+ releaseBarVisibilityForOwner:self withAnimation:YES delay:YES];
+ }
+ return doResign;
+}
+
+// (URLDropTarget protocol)
+- (id<URLDropTargetController>)urlDropController {
+ BrowserWindowController* windowController =
+ [BrowserWindowController browserWindowControllerForView:self];
+ return [windowController toolbarController];
+}
+
+// (URLDropTarget protocol)
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
+ // Make ourself the first responder, which will select the text to indicate
+ // that our contents would be replaced by a drop.
+ // TODO(viettrungluu): crbug.com/30809 -- this is a hack since it steals focus
+ // and doesn't return it.
+ [[self window] makeFirstResponder:self];
+ return [dropHandler_ draggingEntered:sender];
+}
+
+// (URLDropTarget protocol)
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
+ return [dropHandler_ draggingUpdated:sender];
+}
+
+// (URLDropTarget protocol)
+- (void)draggingExited:(id<NSDraggingInfo>)sender {
+ return [dropHandler_ draggingExited:sender];
+}
+
+// (URLDropTarget protocol)
+- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
+ return [dropHandler_ performDragOperation:sender];
+}
+
+- (NSMenu*)decorationMenuForEvent:(NSEvent*)event {
+ AutocompleteTextFieldCell* cell = [self cell];
+ return [cell decorationMenuForEvent:event inRect:[self bounds] ofView:self];
+}
+
+- (ViewID)viewID {
+ return VIEW_ID_LOCATION_BAR;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h
new file mode 100644
index 0000000..1306253
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h
@@ -0,0 +1,76 @@
+// 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 <vector>
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/styled_text_field_cell.h"
+
+@class AutocompleteTextField;
+class LocationBarDecoration;
+
+// AutocompleteTextFieldCell extends StyledTextFieldCell to provide support for
+// certain decorations to be applied to the field. These are the search hint
+// ("Type to search" on the right-hand side), the keyword hint ("Press [Tab] to
+// search Engine" on the right-hand side), and keyword mode ("Search Engine:" in
+// a button-like token on the left-hand side).
+@interface AutocompleteTextFieldCell : StyledTextFieldCell {
+ @private
+ // Decorations which live to the left and right of the text, ordered
+ // from outside in. Decorations are owned by |LocationBarViewMac|.
+ std::vector<LocationBarDecoration*> leftDecorations_;
+ std::vector<LocationBarDecoration*> rightDecorations_;
+}
+
+// Clear |leftDecorations_| and |rightDecorations_|.
+- (void)clearDecorations;
+
+// Add a new left-side decoration to the right of the existing
+// left-side decorations.
+- (void)addLeftDecoration:(LocationBarDecoration*)decoration;
+
+// Add a new right-side decoration to the left of the existing
+// right-side decorations.
+- (void)addRightDecoration:(LocationBarDecoration*)decoration;
+
+// The width available after accounting for decorations.
+- (CGFloat)availableWidthInFrame:(const NSRect)frame;
+
+// Return the frame for |aDecoration| if the cell is in |cellFrame|.
+// Returns |NSZeroRect| for decorations which are not currently
+// visible.
+- (NSRect)frameForDecoration:(const LocationBarDecoration*)aDecoration
+ inFrame:(NSRect)cellFrame;
+
+// Find the decoration under the event. |NULL| if |theEvent| is not
+// over anything.
+- (LocationBarDecoration*)decorationForEvent:(NSEvent*)theEvent
+ inRect:(NSRect)cellFrame
+ ofView:(AutocompleteTextField*)field;
+
+// Return the appropriate menu for any decorations under event.
+// Returns nil if no menu is present for the decoration, or if the
+// event is not over a decoration.
+- (NSMenu*)decorationMenuForEvent:(NSEvent*)theEvent
+ inRect:(NSRect)cellFrame
+ ofView:(AutocompleteTextField*)controlView;
+
+// Called by |AutocompleteTextField| to let page actions intercept
+// clicks. Returns |YES| if the click has been intercepted.
+- (BOOL)mouseDown:(NSEvent*)theEvent
+ inRect:(NSRect)cellFrame
+ ofView:(AutocompleteTextField*)controlView;
+
+// Overridden from StyledTextFieldCell to include decorations adjacent
+// to the text area which don't handle mouse clicks themselves.
+// Keyword-search bubble, for instance.
+- (NSRect)textCursorFrameForFrame:(NSRect)cellFrame;
+
+// Setup decoration tooltips on |controlView| by calling
+// |-addToolTip:forRect:|.
+- (void)updateToolTipsInRect:(NSRect)cellFrame
+ ofView:(AutocompleteTextField*)controlView;
+
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.mm b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.mm
new file mode 100644
index 0000000..02c8a667
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.mm
@@ -0,0 +1,402 @@
+// 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/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
+
+#include "base/logging.h"
+#import "chrome/browser/ui/cocoa/image_utils.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h"
+
+namespace {
+
+const CGFloat kBaselineAdjust = 3.0;
+
+// Matches the clipping radius of |GradientButtonCell|.
+const CGFloat kCornerRadius = 4.0;
+
+// How far to inset the left-hand decorations from the field's bounds.
+const CGFloat kLeftDecorationXOffset = 5.0;
+
+// How far to inset the right-hand decorations from the field's bounds.
+// TODO(shess): Why is this different from |kLeftDecorationXOffset|?
+// |kDecorationOuterXOffset|?
+const CGFloat kRightDecorationXOffset = 5.0;
+
+// The amount of padding on either side reserved for drawing
+// decorations. [Views has |kItemPadding| == 3.]
+const CGFloat kDecorationHorizontalPad = 3.0;
+
+// How long to wait for mouse-up on the location icon before assuming
+// that the user wants to drag.
+const NSTimeInterval kLocationIconDragTimeout = 0.25;
+
+// Calculate the positions for a set of decorations. |frame| is the
+// overall frame to do layout in, |remaining_frame| will get the
+// left-over space. |all_decorations| is the set of decorations to
+// lay out, |decorations| will be set to the decorations which are
+// visible and which fit, in the same order as |all_decorations|,
+// while |decoration_frames| will be the corresponding frames.
+// |x_edge| describes the edge to layout the decorations against
+// (|NSMinXEdge| or |NSMaxXEdge|). |initial_padding| is the padding
+// from the edge of |cell_frame| (|kDecorationHorizontalPad| is used
+// between decorations).
+void CalculatePositionsHelper(
+ NSRect frame,
+ const std::vector<LocationBarDecoration*>& all_decorations,
+ NSRectEdge x_edge,
+ CGFloat initial_padding,
+ std::vector<LocationBarDecoration*>* decorations,
+ std::vector<NSRect>* decoration_frames,
+ NSRect* remaining_frame) {
+ DCHECK(x_edge == NSMinXEdge || x_edge == NSMaxXEdge);
+ DCHECK_EQ(decorations->size(), decoration_frames->size());
+
+ // The outer-most decoration will be inset a bit further from the
+ // edge.
+ CGFloat padding = initial_padding;
+
+ for (size_t i = 0; i < all_decorations.size(); ++i) {
+ if (all_decorations[i]->IsVisible()) {
+ NSRect padding_rect, available;
+
+ // Peel off the outside padding.
+ NSDivideRect(frame, &padding_rect, &available, padding, x_edge);
+
+ // Find out how large the decoration will be in the remaining
+ // space.
+ const CGFloat used_width =
+ all_decorations[i]->GetWidthForSpace(NSWidth(available));
+
+ if (used_width != LocationBarDecoration::kOmittedWidth) {
+ DCHECK_GT(used_width, 0.0);
+ NSRect decoration_frame;
+
+ // Peel off the desired width, leaving the remainder in
+ // |frame|.
+ NSDivideRect(available, &decoration_frame, &frame,
+ used_width, x_edge);
+
+ decorations->push_back(all_decorations[i]);
+ decoration_frames->push_back(decoration_frame);
+ DCHECK_EQ(decorations->size(), decoration_frames->size());
+
+ // Adjust padding for between decorations.
+ padding = kDecorationHorizontalPad;
+ }
+ }
+ }
+
+ DCHECK_EQ(decorations->size(), decoration_frames->size());
+ *remaining_frame = frame;
+}
+
+// Helper function for calculating placement of decorations w/in the
+// cell. |frame| is the cell's boundary rectangle, |remaining_frame|
+// will get any space left after decorations are laid out (for text).
+// |left_decorations| is a set of decorations for the left-hand side
+// of the cell, |right_decorations| for the right-hand side.
+// |decorations| will contain the resulting visible decorations, and
+// |decoration_frames| will contain their frames in the same
+// coordinates as |frame|. Decorations will be ordered left to right.
+// As a convenience returns the index of the first right-hand
+// decoration.
+size_t CalculatePositionsInFrame(
+ NSRect frame,
+ const std::vector<LocationBarDecoration*>& left_decorations,
+ const std::vector<LocationBarDecoration*>& right_decorations,
+ std::vector<LocationBarDecoration*>* decorations,
+ std::vector<NSRect>* decoration_frames,
+ NSRect* remaining_frame) {
+ decorations->clear();
+ decoration_frames->clear();
+
+ // Layout |left_decorations| against the LHS.
+ CalculatePositionsHelper(frame, left_decorations,
+ NSMinXEdge, kLeftDecorationXOffset,
+ decorations, decoration_frames, &frame);
+ DCHECK_EQ(decorations->size(), decoration_frames->size());
+
+ // Capture the number of visible left-hand decorations.
+ const size_t left_count = decorations->size();
+
+ // Layout |right_decorations| against the RHS.
+ CalculatePositionsHelper(frame, right_decorations,
+ NSMaxXEdge, kRightDecorationXOffset,
+ decorations, decoration_frames, &frame);
+ DCHECK_EQ(decorations->size(), decoration_frames->size());
+
+ // Reverse the right-hand decorations so that overall everything is
+ // sorted left to right.
+ std::reverse(decorations->begin() + left_count, decorations->end());
+ std::reverse(decoration_frames->begin() + left_count,
+ decoration_frames->end());
+
+ *remaining_frame = frame;
+ return left_count;
+}
+
+} // namespace
+
+@implementation AutocompleteTextFieldCell
+
+- (CGFloat)baselineAdjust {
+ return kBaselineAdjust;
+}
+
+- (CGFloat)cornerRadius {
+ return kCornerRadius;
+}
+
+- (BOOL)shouldDrawBezel {
+ return YES;
+}
+
+- (void)clearDecorations {
+ leftDecorations_.clear();
+ rightDecorations_.clear();
+}
+
+- (void)addLeftDecoration:(LocationBarDecoration*)decoration {
+ leftDecorations_.push_back(decoration);
+}
+
+- (void)addRightDecoration:(LocationBarDecoration*)decoration {
+ rightDecorations_.push_back(decoration);
+}
+
+- (CGFloat)availableWidthInFrame:(const NSRect)frame {
+ std::vector<LocationBarDecoration*> decorations;
+ std::vector<NSRect> decorationFrames;
+ NSRect textFrame;
+ CalculatePositionsInFrame(frame, leftDecorations_, rightDecorations_,
+ &decorations, &decorationFrames, &textFrame);
+
+ return NSWidth(textFrame);
+}
+
+- (NSRect)frameForDecoration:(const LocationBarDecoration*)aDecoration
+ inFrame:(NSRect)cellFrame {
+ // Short-circuit if the decoration is known to be not visible.
+ if (aDecoration && !aDecoration->IsVisible())
+ return NSZeroRect;
+
+ // Layout the decorations.
+ std::vector<LocationBarDecoration*> decorations;
+ std::vector<NSRect> decorationFrames;
+ NSRect textFrame;
+ CalculatePositionsInFrame(cellFrame, leftDecorations_, rightDecorations_,
+ &decorations, &decorationFrames, &textFrame);
+
+ // Find our decoration and return the corresponding frame.
+ std::vector<LocationBarDecoration*>::const_iterator iter =
+ std::find(decorations.begin(), decorations.end(), aDecoration);
+ if (iter != decorations.end()) {
+ const size_t index = iter - decorations.begin();
+ return decorationFrames[index];
+ }
+
+ // Decorations which are not visible should have been filtered out
+ // at the top, but return |NSZeroRect| rather than a 0-width rect
+ // for consistency.
+ NOTREACHED();
+ return NSZeroRect;
+}
+
+// Overriden to account for the decorations.
+- (NSRect)textFrameForFrame:(NSRect)cellFrame {
+ // Get the frame adjusted for decorations.
+ std::vector<LocationBarDecoration*> decorations;
+ std::vector<NSRect> decorationFrames;
+ NSRect textFrame = [super textFrameForFrame:cellFrame];
+ CalculatePositionsInFrame(textFrame, leftDecorations_, rightDecorations_,
+ &decorations, &decorationFrames, &textFrame);
+
+ // NOTE: This function must closely match the logic in
+ // |-drawInteriorWithFrame:inView:|.
+
+ return textFrame;
+}
+
+- (NSRect)textCursorFrameForFrame:(NSRect)cellFrame {
+ std::vector<LocationBarDecoration*> decorations;
+ std::vector<NSRect> decorationFrames;
+ NSRect textFrame;
+ size_t left_count =
+ CalculatePositionsInFrame(cellFrame, leftDecorations_, rightDecorations_,
+ &decorations, &decorationFrames, &textFrame);
+
+ // Determine the left-most extent for the i-beam cursor.
+ CGFloat minX = NSMinX(textFrame);
+ for (size_t index = left_count; index--; ) {
+ if (decorations[index]->AcceptsMousePress())
+ break;
+
+ // If at leftmost decoration, expand to edge of cell.
+ if (!index) {
+ minX = NSMinX(cellFrame);
+ } else {
+ minX = NSMinX(decorationFrames[index]) - kDecorationHorizontalPad;
+ }
+ }
+
+ // Determine the right-most extent for the i-beam cursor.
+ CGFloat maxX = NSMaxX(textFrame);
+ for (size_t index = left_count; index < decorations.size(); ++index) {
+ if (decorations[index]->AcceptsMousePress())
+ break;
+
+ // If at rightmost decoration, expand to edge of cell.
+ if (index == decorations.size() - 1) {
+ maxX = NSMaxX(cellFrame);
+ } else {
+ maxX = NSMaxX(decorationFrames[index]) + kDecorationHorizontalPad;
+ }
+ }
+
+ // I-beam cursor covers left-most to right-most.
+ return NSMakeRect(minX, NSMinY(textFrame), maxX - minX, NSHeight(textFrame));
+}
+
+- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
+ std::vector<LocationBarDecoration*> decorations;
+ std::vector<NSRect> decorationFrames;
+ NSRect workingFrame;
+ CalculatePositionsInFrame(cellFrame, leftDecorations_, rightDecorations_,
+ &decorations, &decorationFrames, &workingFrame);
+
+ // Draw the decorations.
+ for (size_t i = 0; i < decorations.size(); ++i) {
+ if (decorations[i])
+ decorations[i]->DrawInFrame(decorationFrames[i], controlView);
+ }
+
+ // NOTE: This function must closely match the logic in
+ // |-textFrameForFrame:|.
+
+ // Superclass draws text portion WRT original |cellFrame|.
+ [super drawInteriorWithFrame:cellFrame inView:controlView];
+}
+
+- (LocationBarDecoration*)decorationForEvent:(NSEvent*)theEvent
+ inRect:(NSRect)cellFrame
+ ofView:(AutocompleteTextField*)controlView
+{
+ const BOOL flipped = [controlView isFlipped];
+ const NSPoint location =
+ [controlView convertPoint:[theEvent locationInWindow] fromView:nil];
+
+ std::vector<LocationBarDecoration*> decorations;
+ std::vector<NSRect> decorationFrames;
+ NSRect textFrame;
+ CalculatePositionsInFrame(cellFrame, leftDecorations_, rightDecorations_,
+ &decorations, &decorationFrames, &textFrame);
+
+ for (size_t i = 0; i < decorations.size(); ++i) {
+ if (NSMouseInRect(location, decorationFrames[i], flipped))
+ return decorations[i];
+ }
+
+ return NULL;
+}
+
+- (NSMenu*)decorationMenuForEvent:(NSEvent*)theEvent
+ inRect:(NSRect)cellFrame
+ ofView:(AutocompleteTextField*)controlView {
+ LocationBarDecoration* decoration =
+ [self decorationForEvent:theEvent inRect:cellFrame ofView:controlView];
+ if (decoration)
+ return decoration->GetMenu();
+ return nil;
+}
+
+- (BOOL)mouseDown:(NSEvent*)theEvent
+ inRect:(NSRect)cellFrame
+ ofView:(AutocompleteTextField*)controlView {
+ LocationBarDecoration* decoration =
+ [self decorationForEvent:theEvent inRect:cellFrame ofView:controlView];
+ if (!decoration || !decoration->AcceptsMousePress())
+ return NO;
+
+ NSRect decorationRect =
+ [self frameForDecoration:decoration inFrame:cellFrame];
+
+ // If the decoration is draggable, then initiate a drag if the user
+ // drags or holds the mouse down for awhile.
+ if (decoration->IsDraggable()) {
+ NSDate* timeout =
+ [NSDate dateWithTimeIntervalSinceNow:kLocationIconDragTimeout];
+ NSEvent* event = [NSApp nextEventMatchingMask:(NSLeftMouseDraggedMask |
+ NSLeftMouseUpMask)
+ untilDate:timeout
+ inMode:NSEventTrackingRunLoopMode
+ dequeue:YES];
+ if (!event || [event type] == NSLeftMouseDragged) {
+ NSPasteboard* pboard = decoration->GetDragPasteboard();
+ DCHECK(pboard);
+
+ NSImage* image = decoration->GetDragImage();
+ DCHECK(image);
+
+ NSRect dragImageRect = decoration->GetDragImageFrame(decorationRect);
+
+ // If the original click is not within |dragImageRect|, then
+ // center the image under the mouse. Otherwise, will drag from
+ // where the click was on the image.
+ const NSPoint mousePoint =
+ [controlView convertPoint:[theEvent locationInWindow] fromView:nil];
+ if (!NSMouseInRect(mousePoint, dragImageRect, [controlView isFlipped])) {
+ dragImageRect.origin =
+ NSMakePoint(mousePoint.x - NSWidth(dragImageRect) / 2.0,
+ mousePoint.y - NSHeight(dragImageRect) / 2.0);
+ }
+
+ // -[NSView dragImage:at:*] wants the images lower-left point,
+ // regardless of -isFlipped. Converting the rect to window base
+ // coordinates doesn't require any special-casing. Note that
+ // -[NSView dragFile:fromRect:*] takes a rect rather than a
+ // point, likely for this exact reason.
+ const NSPoint dragPoint =
+ [controlView convertRect:dragImageRect toView:nil].origin;
+ [[controlView window] dragImage:image
+ at:dragPoint
+ offset:NSZeroSize
+ event:theEvent
+ pasteboard:pboard
+ source:self
+ slideBack:YES];
+
+ return YES;
+ }
+
+ // On mouse-up fall through to mouse-pressed case.
+ DCHECK_EQ([event type], NSLeftMouseUp);
+ }
+
+ if (!decoration->OnMousePressed(decorationRect))
+ return NO;
+
+ return YES;
+}
+
+- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
+ return NSDragOperationCopy;
+}
+
+- (void)updateToolTipsInRect:(NSRect)cellFrame
+ ofView:(AutocompleteTextField*)controlView {
+ std::vector<LocationBarDecoration*> decorations;
+ std::vector<NSRect> decorationFrames;
+ NSRect textFrame;
+ CalculatePositionsInFrame(cellFrame, leftDecorations_, rightDecorations_,
+ &decorations, &decorationFrames, &textFrame);
+
+ for (size_t i = 0; i < decorations.size(); ++i) {
+ NSString* tooltip = decorations[i]->GetToolTip();
+ if ([tooltip length] > 0)
+ [controlView addToolTip:tooltip forRect:decorationFrames[i]];
+ }
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell_unittest.mm b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell_unittest.mm
new file mode 100644
index 0000000..1598cad
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell_unittest.mm
@@ -0,0 +1,300 @@
+// 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 "app/resource_bundle.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
+#import "chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/star_decoration.h"
+#include "grit/theme_resources.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+
+using ::testing::Return;
+using ::testing::StrictMock;
+using ::testing::_;
+
+namespace {
+
+// Width of the field so that we don't have to ask |field_| for it all
+// the time.
+const CGFloat kWidth(300.0);
+
+// A narrow width for tests which test things that don't fit.
+const CGFloat kNarrowWidth(5.0);
+
+class MockDecoration : public LocationBarDecoration {
+ public:
+ virtual CGFloat GetWidthForSpace(CGFloat width) { return 20.0; }
+
+ MOCK_METHOD2(DrawInFrame, void(NSRect frame, NSView* control_view));
+ MOCK_METHOD0(GetToolTip, NSString*());
+};
+
+class AutocompleteTextFieldCellTest : public CocoaTest {
+ public:
+ AutocompleteTextFieldCellTest() {
+ // Make sure this is wide enough to play games with the cell
+ // decorations.
+ const NSRect frame = NSMakeRect(0, 0, kWidth, 30);
+
+ scoped_nsobject<NSTextField> view(
+ [[NSTextField alloc] initWithFrame:frame]);
+ view_ = view.get();
+
+ scoped_nsobject<AutocompleteTextFieldCell> cell(
+ [[AutocompleteTextFieldCell alloc] initTextCell:@"Testing"]);
+ [cell setEditable:YES];
+ [cell setBordered:YES];
+
+ [cell clearDecorations];
+ mock_left_decoration_.SetVisible(false);
+ [cell addLeftDecoration:&mock_left_decoration_];
+ mock_right_decoration0_.SetVisible(false);
+ mock_right_decoration1_.SetVisible(false);
+ [cell addRightDecoration:&mock_right_decoration0_];
+ [cell addRightDecoration:&mock_right_decoration1_];
+
+ [view_ setCell:cell.get()];
+
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ NSTextField* view_;
+ MockDecoration mock_left_decoration_;
+ MockDecoration mock_right_decoration0_;
+ MockDecoration mock_right_decoration1_;
+};
+
+// Basic view tests (AddRemove, Display).
+TEST_VIEW(AutocompleteTextFieldCellTest, view_);
+
+// Test drawing, mostly to ensure nothing leaks or crashes.
+// Flaky, disabled. Bug http://crbug.com/49522
+TEST_F(AutocompleteTextFieldCellTest, DISABLED_FocusedDisplay) {
+ [view_ display];
+
+ // Test focused drawing.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:view_];
+ [view_ display];
+ [test_window() clearPretendKeyWindowAndFirstResponder];
+
+ // Test display of various cell configurations.
+ AutocompleteTextFieldCell* cell =
+ static_cast<AutocompleteTextFieldCell*>([view_ cell]);
+
+ // Load available decorations and try drawing. To make sure that
+ // they are actually drawn, check that |GetWidthForSpace()| doesn't
+ // indicate that they should be omitted.
+ const CGFloat kVeryWide = 1000.0;
+
+ SelectedKeywordDecoration selected_keyword_decoration([view_ font]);
+ selected_keyword_decoration.SetVisible(true);
+ selected_keyword_decoration.SetKeyword(std::wstring(L"Google"), false);
+ [cell addLeftDecoration:&selected_keyword_decoration];
+ EXPECT_NE(selected_keyword_decoration.GetWidthForSpace(kVeryWide),
+ LocationBarDecoration::kOmittedWidth);
+
+ // TODO(shess): This really wants a |LocationBarViewMac|, but only a
+ // few methods reference it, so this works well enough. But
+ // something better would be nice.
+ LocationIconDecoration location_icon_decoration(NULL);
+ location_icon_decoration.SetVisible(true);
+ location_icon_decoration.SetImage([NSImage imageNamed:@"NSApplicationIcon"]);
+ [cell addLeftDecoration:&location_icon_decoration];
+ EXPECT_NE(location_icon_decoration.GetWidthForSpace(kVeryWide),
+ LocationBarDecoration::kOmittedWidth);
+
+ EVBubbleDecoration ev_bubble_decoration(&location_icon_decoration,
+ [view_ font]);
+ ev_bubble_decoration.SetVisible(true);
+ ev_bubble_decoration.SetImage([NSImage imageNamed:@"NSApplicationIcon"]);
+ ev_bubble_decoration.SetLabel(@"Application");
+ [cell addLeftDecoration:&ev_bubble_decoration];
+ EXPECT_NE(ev_bubble_decoration.GetWidthForSpace(kVeryWide),
+ LocationBarDecoration::kOmittedWidth);
+
+ StarDecoration star_decoration(NULL);
+ star_decoration.SetVisible(true);
+ [cell addRightDecoration:&star_decoration];
+ EXPECT_NE(star_decoration.GetWidthForSpace(kVeryWide),
+ LocationBarDecoration::kOmittedWidth);
+
+ KeywordHintDecoration keyword_hint_decoration([view_ font]);
+ keyword_hint_decoration.SetVisible(true);
+ keyword_hint_decoration.SetKeyword(std::wstring(L"google"), false);
+ [cell addRightDecoration:&keyword_hint_decoration];
+ EXPECT_NE(keyword_hint_decoration.GetWidthForSpace(kVeryWide),
+ LocationBarDecoration::kOmittedWidth);
+
+ // Make sure we're actually calling |DrawInFrame()|.
+ StrictMock<MockDecoration> mock_decoration;
+ mock_decoration.SetVisible(true);
+ [cell addLeftDecoration:&mock_decoration];
+ EXPECT_CALL(mock_decoration, DrawInFrame(_, _));
+ EXPECT_NE(mock_decoration.GetWidthForSpace(kVeryWide),
+ LocationBarDecoration::kOmittedWidth);
+
+ [view_ display];
+
+ [cell clearDecorations];
+}
+
+TEST_F(AutocompleteTextFieldCellTest, TextFrame) {
+ AutocompleteTextFieldCell* cell =
+ static_cast<AutocompleteTextFieldCell*>([view_ cell]);
+ const NSRect bounds([view_ bounds]);
+ NSRect textFrame;
+
+ // The cursor frame should stay the same throughout.
+ const NSRect cursorFrame([cell textCursorFrameForFrame:bounds]);
+ EXPECT_TRUE(NSEqualRects(cursorFrame, bounds));
+
+ // At default settings, everything goes to the text area.
+ textFrame = [cell textFrameForFrame:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(textFrame));
+ EXPECT_TRUE(NSContainsRect(bounds, textFrame));
+ EXPECT_EQ(NSMinX(bounds), NSMinX(textFrame));
+ EXPECT_EQ(NSMaxX(bounds), NSMaxX(textFrame));
+ EXPECT_TRUE(NSContainsRect(cursorFrame, textFrame));
+
+ // Decoration on the left takes up space.
+ mock_left_decoration_.SetVisible(true);
+ textFrame = [cell textFrameForFrame:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(textFrame));
+ EXPECT_TRUE(NSContainsRect(bounds, textFrame));
+ EXPECT_GT(NSMinX(textFrame), NSMinX(bounds));
+ EXPECT_TRUE(NSContainsRect(cursorFrame, textFrame));
+}
+
+// The editor frame should be slightly inset from the text frame.
+TEST_F(AutocompleteTextFieldCellTest, DrawingRectForBounds) {
+ AutocompleteTextFieldCell* cell =
+ static_cast<AutocompleteTextFieldCell*>([view_ cell]);
+ const NSRect bounds([view_ bounds]);
+ NSRect textFrame, drawingRect;
+
+ textFrame = [cell textFrameForFrame:bounds];
+ drawingRect = [cell drawingRectForBounds:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(drawingRect));
+ EXPECT_TRUE(NSContainsRect(textFrame, NSInsetRect(drawingRect, 1, 1)));
+
+ // Save the starting frame for after clear.
+ const NSRect originalDrawingRect = drawingRect;
+
+ mock_left_decoration_.SetVisible(true);
+ textFrame = [cell textFrameForFrame:bounds];
+ drawingRect = [cell drawingRectForBounds:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(drawingRect));
+ EXPECT_TRUE(NSContainsRect(NSInsetRect(textFrame, 1, 1), drawingRect));
+
+ mock_right_decoration0_.SetVisible(true);
+ textFrame = [cell textFrameForFrame:bounds];
+ drawingRect = [cell drawingRectForBounds:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(drawingRect));
+ EXPECT_TRUE(NSContainsRect(NSInsetRect(textFrame, 1, 1), drawingRect));
+
+ mock_left_decoration_.SetVisible(false);
+ mock_right_decoration0_.SetVisible(false);
+ drawingRect = [cell drawingRectForBounds:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(drawingRect));
+ EXPECT_TRUE(NSEqualRects(drawingRect, originalDrawingRect));
+}
+
+// Test that left decorations are at the correct edge of the cell.
+TEST_F(AutocompleteTextFieldCellTest, LeftDecorationFrame) {
+ AutocompleteTextFieldCell* cell =
+ static_cast<AutocompleteTextFieldCell*>([view_ cell]);
+ const NSRect bounds = [view_ bounds];
+
+ mock_left_decoration_.SetVisible(true);
+ const NSRect decorationRect =
+ [cell frameForDecoration:&mock_left_decoration_ inFrame:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(decorationRect));
+ EXPECT_TRUE(NSContainsRect(bounds, decorationRect));
+
+ // Decoration should be left of |drawingRect|.
+ const NSRect drawingRect = [cell drawingRectForBounds:bounds];
+ EXPECT_GT(NSMinX(drawingRect), NSMinX(decorationRect));
+
+ // Decoration should be left of |textFrame|.
+ const NSRect textFrame = [cell textFrameForFrame:bounds];
+ EXPECT_GT(NSMinX(textFrame), NSMinX(decorationRect));
+}
+
+// Test that right decorations are at the correct edge of the cell.
+TEST_F(AutocompleteTextFieldCellTest, RightDecorationFrame) {
+ AutocompleteTextFieldCell* cell =
+ static_cast<AutocompleteTextFieldCell*>([view_ cell]);
+ const NSRect bounds = [view_ bounds];
+
+ mock_right_decoration0_.SetVisible(true);
+ mock_right_decoration1_.SetVisible(true);
+
+ const NSRect decoration0Rect =
+ [cell frameForDecoration:&mock_right_decoration0_ inFrame:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(decoration0Rect));
+ EXPECT_TRUE(NSContainsRect(bounds, decoration0Rect));
+
+ // Right-side decorations are ordered from rightmost to leftmost.
+ // Outer decoration (0) to right of inner decoration (1).
+ const NSRect decoration1Rect =
+ [cell frameForDecoration:&mock_right_decoration1_ inFrame:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(decoration1Rect));
+ EXPECT_TRUE(NSContainsRect(bounds, decoration1Rect));
+ EXPECT_LT(NSMinX(decoration1Rect), NSMinX(decoration0Rect));
+
+ // Decoration should be right of |drawingRect|.
+ const NSRect drawingRect = [cell drawingRectForBounds:bounds];
+ EXPECT_LT(NSMinX(drawingRect), NSMinX(decoration1Rect));
+
+ // Decoration should be right of |textFrame|.
+ const NSRect textFrame = [cell textFrameForFrame:bounds];
+ EXPECT_LT(NSMinX(textFrame), NSMinX(decoration1Rect));
+}
+
+// Verify -[AutocompleteTextFieldCell updateToolTipsInRect:ofView:].
+TEST_F(AutocompleteTextFieldCellTest, UpdateToolTips) {
+ NSString* tooltip = @"tooltip";
+
+ // Left decoration returns a tooltip, make sure it is called at
+ // least once.
+ mock_left_decoration_.SetVisible(true);
+ EXPECT_CALL(mock_left_decoration_, GetToolTip())
+ .WillOnce(Return(tooltip))
+ .WillRepeatedly(Return(tooltip));
+
+ // Right decoration returns no tooltip, make sure it is called at
+ // least once.
+ mock_right_decoration0_.SetVisible(true);
+ EXPECT_CALL(mock_right_decoration0_, GetToolTip())
+ .WillOnce(Return((NSString*)nil))
+ .WillRepeatedly(Return((NSString*)nil));
+
+ AutocompleteTextFieldCell* cell =
+ static_cast<AutocompleteTextFieldCell*>([view_ cell]);
+ const NSRect bounds = [view_ bounds];
+ const NSRect leftDecorationRect =
+ [cell frameForDecoration:&mock_left_decoration_ inFrame:bounds];
+
+ // |controlView| gets the tooltip for the left decoration.
+ id controlView = [OCMockObject mockForClass:[AutocompleteTextField class]];
+ [[controlView expect] addToolTip:tooltip forRect:leftDecorationRect];
+
+ [cell updateToolTipsInRect:bounds ofView:controlView];
+
+ [controlView verify];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h
new file mode 100644
index 0000000..905bc84
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h
@@ -0,0 +1,56 @@
+// 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/ui/cocoa/url_drop_target.h"
+
+@class AutocompleteTextField;
+class AutocompleteTextFieldObserver;
+
+// AutocompleteTextFieldEditor customized the AutocompletTextField
+// field editor (helper text-view used in editing). It intercepts UI
+// events for forwarding to the core Omnibox code. It also undoes
+// some of the effects of using styled text in the Omnibox (the text
+// is styled but should not appear that way when copied to the
+// pasteboard).
+
+// Field editor used for the autocomplete field.
+@interface AutocompleteTextFieldEditor : NSTextView<URLDropTarget> {
+ // Handles being a drag-and-drop target. We handle DnD directly instead
+ // allowing the |AutocompletTextField| to handle it (by making an empty
+ // |-updateDragTypeRegistration|), since the latter results in a weird
+ // start-up time regression.
+ scoped_nsobject<URLDropTargetHandler> dropHandler_;
+
+ scoped_nsobject<NSCharacterSet> forbiddenCharacters_;
+
+ // Indicates if the field editor's interpretKeyEvents: method is being called.
+ // If it's YES, then we should postpone the call to the observer's
+ // OnDidChange() method after the field editor's interpretKeyEvents: method
+ // is finished, rather than calling it in textDidChange: method. Because the
+ // input method may update the marked text after inserting some text, but we
+ // need the observer be aware of the marked text as well.
+ BOOL interpretingKeyEvents_;
+
+ // Indicates if the text has been changed by key events.
+ BOOL textChangedByKeyEvents_;
+}
+
+// The delegate is always an AutocompleteTextField*. Override the superclass
+// implementations to allow for proper typing.
+- (AutocompleteTextField*)delegate;
+- (void)setDelegate:(AutocompleteTextField*)delegate;
+
+// Sets attributed string programatically through the field editor's text
+// storage object.
+- (void)setAttributedString:(NSAttributedString*)aString;
+
+@end
+
+@interface AutocompleteTextFieldEditor(PrivateTestMethods)
+- (AutocompleteTextFieldObserver*)observer;
+- (void)pasteAndGo:sender;
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.mm b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.mm
new file mode 100644
index 0000000..1b52e70
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.mm
@@ -0,0 +1,371 @@
+// 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/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
+
+#include "app/l10n_util_mac.h"
+#include "base/string_util.h"
+#include "grit/generated_resources.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h" // IDC_*
+#include "chrome/browser/browser_list.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
+#import "chrome/browser/ui/cocoa/toolbar_controller.h"
+
+@implementation AutocompleteTextFieldEditor
+
+- (id)initWithFrame:(NSRect)frameRect {
+ if ((self = [super initWithFrame:frameRect])) {
+ dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]);
+
+ forbiddenCharacters_.reset([[NSCharacterSet controlCharacterSet] retain]);
+ }
+ return self;
+}
+
+// If the entire field is selected, drag the same data as would be
+// dragged from the field's location icon. In some cases the textual
+// contents will not contain relevant data (for instance, "http://" is
+// stripped from URLs).
+- (BOOL)dragSelectionWithEvent:(NSEvent *)event
+ offset:(NSSize)mouseOffset
+ slideBack:(BOOL)slideBack {
+ AutocompleteTextFieldObserver* observer = [self observer];
+ DCHECK(observer);
+ if (observer && observer->CanCopy()) {
+ NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
+ observer->CopyToPasteboard(pboard);
+
+ NSPoint p;
+ NSImage* image = [self dragImageForSelectionWithEvent:event origin:&p];
+
+ [self dragImage:image
+ at:p
+ offset:mouseOffset
+ event:event
+ pasteboard:pboard
+ source:self
+ slideBack:slideBack];
+ return YES;
+ }
+ return [super dragSelectionWithEvent:event
+ offset:mouseOffset
+ slideBack:slideBack];
+}
+
+- (void)copy:(id)sender {
+ AutocompleteTextFieldObserver* observer = [self observer];
+ DCHECK(observer);
+ if (observer && observer->CanCopy())
+ observer->CopyToPasteboard([NSPasteboard generalPasteboard]);
+}
+
+- (void)cut:(id)sender {
+ [self copy:sender];
+ [self delete:nil];
+}
+
+// This class assumes that the delegate is an AutocompleteTextField.
+// Enforce that assumption.
+- (AutocompleteTextField*)delegate {
+ AutocompleteTextField* delegate =
+ static_cast<AutocompleteTextField*>([super delegate]);
+ DCHECK(delegate == nil ||
+ [delegate isKindOfClass:[AutocompleteTextField class]]);
+ return delegate;
+}
+
+- (void)setDelegate:(AutocompleteTextField*)delegate {
+ DCHECK(delegate == nil ||
+ [delegate isKindOfClass:[AutocompleteTextField class]]);
+ [super setDelegate:delegate];
+}
+
+// Convenience method for retrieving the observer from the delegate.
+- (AutocompleteTextFieldObserver*)observer {
+ return [[self delegate] observer];
+}
+
+- (void)paste:(id)sender {
+ AutocompleteTextFieldObserver* observer = [self observer];
+ DCHECK(observer);
+ if (observer) {
+ observer->OnPaste();
+ }
+}
+
+- (void)pasteAndGo:sender {
+ AutocompleteTextFieldObserver* observer = [self observer];
+ DCHECK(observer);
+ if (observer) {
+ observer->OnPasteAndGo();
+ }
+}
+
+// We have rich text, but it shouldn't be modified by the user, so
+// don't update the font panel. In theory, -setUsesFontPanel: should
+// accomplish this, but that gets called frequently with YES when
+// NSTextField and NSTextView synchronize their contents. That is
+// probably unavoidable because in most cases having rich text in the
+// field you probably would expect it to update the font panel.
+- (void)updateFontPanel {}
+
+// No ruler bar, so don't update any of that state, either.
+- (void)updateRuler {}
+
+- (NSMenu*)menuForEvent:(NSEvent*)event {
+ // Give the control a chance to provide page-action menus.
+ // NOTE: Note that page actions aren't even in the editor's
+ // boundaries! The Cocoa control implementation seems to do a
+ // blanket forward to here if nothing more specific is returned from
+ // the control and cell calls.
+ // TODO(shess): Determine if the page-action part of this can be
+ // moved to the cell.
+ NSMenu* actionMenu = [[self delegate] decorationMenuForEvent:event];
+ if (actionMenu)
+ return actionMenu;
+
+ NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"TITLE"] autorelease];
+ [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_CUT)
+ action:@selector(cut:)
+ keyEquivalent:@""];
+ [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_COPY)
+ action:@selector(copy:)
+ keyEquivalent:@""];
+ [menu addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_PASTE)
+ action:@selector(paste:)
+ keyEquivalent:@""];
+
+ // TODO(shess): If the control is not editable, should we show a
+ // greyed-out "Paste and Go"?
+ if ([self isEditable]) {
+ // Paste and go/search.
+ AutocompleteTextFieldObserver* observer = [self observer];
+ DCHECK(observer);
+ if (observer && observer->CanPasteAndGo()) {
+ const int string_id = observer->GetPasteActionStringId();
+ NSString* label = l10n_util::GetNSStringWithFixup(string_id);
+
+ // TODO(rohitrao): If the clipboard is empty, should we show a
+ // greyed-out "Paste and Go" or nothing at all?
+ if ([label length]) {
+ [menu addItemWithTitle:label
+ action:@selector(pasteAndGo:)
+ keyEquivalent:@""];
+ }
+ }
+
+ NSString* label = l10n_util::GetNSStringWithFixup(IDS_EDIT_SEARCH_ENGINES);
+ DCHECK([label length]);
+ if ([label length]) {
+ [menu addItem:[NSMenuItem separatorItem]];
+ NSMenuItem* item = [menu addItemWithTitle:label
+ action:@selector(commandDispatch:)
+ keyEquivalent:@""];
+ [item setTag:IDC_EDIT_SEARCH_ENGINES];
+ }
+ }
+
+ return menu;
+}
+
+// (Overridden from NSResponder)
+- (BOOL)becomeFirstResponder {
+ BOOL doAccept = [super becomeFirstResponder];
+ AutocompleteTextField* field = [self delegate];
+ // Only lock visibility if we've been set up with a delegate (the text field).
+ if (doAccept && field) {
+ // Give the text field ownership of the visibility lock. (The first
+ // responder dance between the field and the field editor is a little
+ // weird.)
+ [[BrowserWindowController browserWindowControllerForView:field]
+ lockBarVisibilityForOwner:field withAnimation:YES delay:NO];
+ }
+ return doAccept;
+}
+
+// (Overridden from NSResponder)
+- (BOOL)resignFirstResponder {
+ BOOL doResign = [super resignFirstResponder];
+ AutocompleteTextField* field = [self delegate];
+ // Only lock visibility if we've been set up with a delegate (the text field).
+ if (doResign && field) {
+ // Give the text field ownership of the visibility lock.
+ [[BrowserWindowController browserWindowControllerForView:field]
+ releaseBarVisibilityForOwner:field withAnimation:YES delay:YES];
+
+ AutocompleteTextFieldObserver* observer = [self observer];
+ if (observer)
+ observer->OnKillFocus();
+ }
+ return doResign;
+}
+
+// (URLDropTarget protocol)
+- (id<URLDropTargetController>)urlDropController {
+ BrowserWindowController* windowController =
+ [BrowserWindowController browserWindowControllerForView:self];
+ return [windowController toolbarController];
+}
+
+// (URLDropTarget protocol)
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
+ // Make ourself the first responder (even though we're presumably already the
+ // first responder), which will select the text to indicate that our contents
+ // would be replaced by a drop.
+ [[self window] makeFirstResponder:self];
+ return [dropHandler_ draggingEntered:sender];
+}
+
+// (URLDropTarget protocol)
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
+ return [dropHandler_ draggingUpdated:sender];
+}
+
+// (URLDropTarget protocol)
+- (void)draggingExited:(id<NSDraggingInfo>)sender {
+ return [dropHandler_ draggingExited:sender];
+}
+
+// (URLDropTarget protocol)
+- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
+ return [dropHandler_ performDragOperation:sender];
+}
+
+// Prevent control characters from being entered into the Omnibox.
+// This is invoked for keyboard entry, not for pasting.
+- (void)insertText:(id)aString {
+ // This method is documented as received either |NSString| or
+ // |NSAttributedString|. The autocomplete code will restyle the
+ // results in any case, so simplify by always using |NSString|.
+ if ([aString isKindOfClass:[NSAttributedString class]])
+ aString = [aString string];
+
+ // Repeatedly remove control characters. The loop will only ever
+ // execute at allwhen the user enters control characters (using
+ // Ctrl-Alt- or Ctrl-Q). Making this generally efficient would
+ // probably be a loss, since the input always seems to be a single
+ // character.
+ NSRange range = [aString rangeOfCharacterFromSet:forbiddenCharacters_];
+ while (range.location != NSNotFound) {
+ aString = [aString stringByReplacingCharactersInRange:range withString:@""];
+ range = [aString rangeOfCharacterFromSet:forbiddenCharacters_];
+ }
+ DCHECK_EQ(range.length, 0U);
+
+ // NOTE: If |aString| is empty, this intentionally replaces the
+ // selection with empty. This seems consistent with the case where
+ // the input contained a mixture of characters and the string ended
+ // up not empty.
+ [super insertText:aString];
+}
+
+- (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange {
+ [super setMarkedText:aString selectedRange:selRange];
+
+ // Because the AutocompleteEditViewMac class treats marked text as content,
+ // we need to treat the change to marked text as content change as well.
+ [self didChangeText];
+}
+
+- (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange
+ granularity:(NSSelectionGranularity)granularity {
+ AutocompleteTextFieldObserver* observer = [self observer];
+ NSRange modifiedRange = [super selectionRangeForProposedRange:proposedSelRange
+ granularity:granularity];
+ if (observer)
+ return observer->SelectionRangeForProposedRange(modifiedRange);
+ return modifiedRange;
+}
+
+
+
+
+- (void)setSelectedRange:(NSRange)charRange
+ affinity:(NSSelectionAffinity)affinity
+ stillSelecting:(BOOL)flag {
+ [super setSelectedRange:charRange affinity:affinity stillSelecting:flag];
+
+ // We're only interested in selection changes directly caused by keyboard
+ // input from the user.
+ if (interpretingKeyEvents_)
+ textChangedByKeyEvents_ = YES;
+}
+
+- (void)interpretKeyEvents:(NSArray *)eventArray {
+ DCHECK(!interpretingKeyEvents_);
+ interpretingKeyEvents_ = YES;
+ textChangedByKeyEvents_ = NO;
+ [super interpretKeyEvents:eventArray];
+
+ AutocompleteTextFieldObserver* observer = [self observer];
+ if (textChangedByKeyEvents_ && observer)
+ observer->OnDidChange();
+
+ DCHECK(interpretingKeyEvents_);
+ interpretingKeyEvents_ = NO;
+}
+
+- (void)didChangeText {
+ [super didChangeText];
+
+ AutocompleteTextFieldObserver* observer = [self observer];
+ if (observer) {
+ if (!interpretingKeyEvents_)
+ observer->OnDidChange();
+ else
+ textChangedByKeyEvents_ = YES;
+ }
+}
+
+- (void)doCommandBySelector:(SEL)cmd {
+ // TODO(shess): Review code for cases where we're fruitlessly attempting to
+ // work in spite of not having an observer.
+ AutocompleteTextFieldObserver* observer = [self observer];
+
+ if (observer && observer->OnDoCommandBySelector(cmd)) {
+ // The observer should already be aware of any changes to the text, so
+ // setting |textChangedByKeyEvents_| to NO to prevent its OnDidChange()
+ // method from being called unnecessarily.
+ textChangedByKeyEvents_ = NO;
+ return;
+ }
+
+ // If the escape key was pressed and no revert happened and we're in
+ // fullscreen mode, make it resign key.
+ if (cmd == @selector(cancelOperation:)) {
+ BrowserWindowController* windowController =
+ [BrowserWindowController browserWindowControllerForView:self];
+ if ([windowController isFullscreen]) {
+ [windowController focusTabContents];
+ return;
+ }
+ }
+
+ [super doCommandBySelector:cmd];
+}
+
+- (void)setAttributedString:(NSAttributedString*)aString {
+ NSTextStorage* textStorage = [self textStorage];
+ DCHECK(textStorage);
+ [textStorage setAttributedString:aString];
+
+ // The text has been changed programmatically. The observer should know
+ // this change, so setting |textChangedByKeyEvents_| to NO to
+ // prevent its OnDidChange() method from being called unnecessarily.
+ textChangedByKeyEvents_ = NO;
+}
+
+- (void)mouseDown:(NSEvent*)theEvent {
+ // Close the popup before processing the event.
+ AutocompleteTextFieldObserver* observer = [self observer];
+ if (observer)
+ observer->ClosePopup();
+
+ [super mouseDown:theEvent];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor_unittest.mm b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor_unittest.mm
new file mode 100644
index 0000000..e43b734
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor_unittest.mm
@@ -0,0 +1,297 @@
+// 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/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
+
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "base/string_util.h"
+#include "chrome/app/chrome_command_ids.h" // IDC_*
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.h"
+#import "chrome/browser/ui/cocoa/test_event_utils.h"
+#include "grit/generated_resources.h"
+#include "testing/gmock/include/gmock/gmock-matchers.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+
+using ::testing::Return;
+using ::testing::ReturnArg;
+using ::testing::StrictMock;
+using ::testing::A;
+
+namespace {
+
+// TODO(shess): Very similar to AutocompleteTextFieldTest. Maybe
+// those can be shared.
+
+class AutocompleteTextFieldEditorTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ NSRect frame = NSMakeRect(0, 0, 50, 30);
+ scoped_nsobject<AutocompleteTextField> field(
+ [[AutocompleteTextField alloc] initWithFrame:frame]);
+ field_ = field.get();
+ [field_ setStringValue:@"Testing"];
+ [[test_window() contentView] addSubview:field_];
+
+ // Arrange for |field_| to get the right field editor.
+ window_delegate_.reset(
+ [[AutocompleteTextFieldWindowTestDelegate alloc] init]);
+ [test_window() setDelegate:window_delegate_.get()];
+
+ // Get the field editor setup.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ editor_ = static_cast<AutocompleteTextFieldEditor*>([field_ currentEditor]);
+
+ EXPECT_TRUE(editor_);
+ EXPECT_TRUE([editor_ isKindOfClass:[AutocompleteTextFieldEditor class]]);
+ }
+
+ AutocompleteTextFieldEditor* editor_;
+ AutocompleteTextField* field_;
+ scoped_nsobject<AutocompleteTextFieldWindowTestDelegate> window_delegate_;
+};
+
+// Disabled because it crashes sometimes. http://crbug.com/49522
+// Can't rename DISABLED_ because the TEST_VIEW macro prepends.
+// http://crbug.com/53621
+#if 0
+TEST_VIEW(AutocompleteTextFieldEditorTest, field_);
+#endif
+
+// Test that control characters are stripped from insertions.
+TEST_F(AutocompleteTextFieldEditorTest, InsertStripsControlChars) {
+ // Sets a string in the field.
+ NSString* test_string = @"astring";
+ [field_ setStringValue:test_string];
+ [editor_ selectAll:nil];
+
+ [editor_ insertText:@"t"];
+ EXPECT_NSEQ(@"t", [field_ stringValue]);
+
+ [editor_ insertText:@"h"];
+ EXPECT_NSEQ(@"th", [field_ stringValue]);
+
+ // TAB doesn't get inserted.
+ [editor_ insertText:[NSString stringWithFormat:@"%c", 7]];
+ EXPECT_NSEQ(@"th", [field_ stringValue]);
+
+ // Newline doesn't get inserted.
+ [editor_ insertText:[NSString stringWithFormat:@"%c", 12]];
+ EXPECT_NSEQ(@"th", [field_ stringValue]);
+
+ // Multi-character strings get through.
+ [editor_ insertText:[NSString stringWithFormat:@"i%cs%c", 8, 127]];
+ EXPECT_NSEQ(@"this", [field_ stringValue]);
+
+ // Attempting to insert newline when everything is selected clears
+ // the field.
+ [editor_ selectAll:nil];
+ [editor_ insertText:[NSString stringWithFormat:@"%c", 12]];
+ EXPECT_NSEQ(@"", [field_ stringValue]);
+}
+
+// Test that |delegate| can provide page action menus.
+TEST_F(AutocompleteTextFieldEditorTest, PageActionMenus) {
+ // The event just needs to be something the mock can recognize.
+ NSEvent* event =
+ test_event_utils::MouseEventAtPoint(NSZeroPoint, NSRightMouseDown, 0);
+
+ // Trivial menu which we can recognize and which doesn't look like
+ // the default editor context menu.
+ scoped_nsobject<id> menu([[NSMenu alloc] initWithTitle:@"Menu"]);
+ [menu addItemWithTitle:@"Go Fish"
+ action:@selector(goFish:)
+ keyEquivalent:@""];
+
+ // For OCMOCK_VALUE().
+ BOOL yes = YES;
+
+ // So that we don't have to mock the observer.
+ [editor_ setEditable:NO];
+
+ // The delegate's intercept point gets called, and results are
+ // propagated back.
+ {
+ id delegate = [OCMockObject mockForClass:[AutocompleteTextField class]];
+ [[[delegate stub] andReturnValue:OCMOCK_VALUE(yes)]
+ isKindOfClass:[AutocompleteTextField class]];
+ [[[delegate expect] andReturn:menu.get()] decorationMenuForEvent:event];
+ [editor_ setDelegate:delegate];
+ NSMenu* contextMenu = [editor_ menuForEvent:event];
+ [delegate verify];
+ [editor_ setDelegate:nil];
+
+ EXPECT_EQ(contextMenu, menu.get());
+ }
+
+ // If the delegate does not return any menu, the default menu is
+ // returned.
+ {
+ id delegate = [OCMockObject mockForClass:[AutocompleteTextField class]];
+ [[[delegate stub] andReturnValue:OCMOCK_VALUE(yes)]
+ isKindOfClass:[AutocompleteTextField class]];
+ [[[delegate expect] andReturn:nil] decorationMenuForEvent:event];
+ [editor_ setDelegate:delegate];
+ NSMenu* contextMenu = [editor_ menuForEvent:event];
+ [delegate verify];
+ [editor_ setDelegate:nil];
+
+ EXPECT_NE(contextMenu, menu.get());
+ NSArray* items = [contextMenu itemArray];
+ ASSERT_GT([items count], 0U);
+ EXPECT_EQ(@selector(cut:), [[items objectAtIndex:0] action])
+ << "action is: " << sel_getName([[items objectAtIndex:0] action]);
+ }
+}
+
+// Base class for testing AutocompleteTextFieldObserver messages.
+class AutocompleteTextFieldEditorObserverTest
+ : public AutocompleteTextFieldEditorTest {
+ public:
+ virtual void SetUp() {
+ AutocompleteTextFieldEditorTest::SetUp();
+ [field_ setObserver:&field_observer_];
+ }
+
+ virtual void TearDown() {
+ // Clear the observer so that we don't show output for
+ // uninteresting messages to the mock (for instance, if |field_| has
+ // focus at the end of the test).
+ [field_ setObserver:NULL];
+
+ AutocompleteTextFieldEditorTest::TearDown();
+ }
+
+ StrictMock<MockAutocompleteTextFieldObserver> field_observer_;
+};
+
+// Test that the field editor is linked in correctly.
+TEST_F(AutocompleteTextFieldEditorTest, FirstResponder) {
+ EXPECT_EQ(editor_, [field_ currentEditor]);
+ EXPECT_TRUE([editor_ isDescendantOf:field_]);
+ EXPECT_EQ([editor_ delegate], field_);
+ EXPECT_EQ([editor_ observer], [field_ observer]);
+}
+
+// Test drawing, mostly to ensure nothing leaks or crashes.
+TEST_F(AutocompleteTextFieldEditorTest, Display) {
+ [field_ display];
+ [editor_ display];
+}
+
+// Test that -paste: is correctly delegated to the observer.
+TEST_F(AutocompleteTextFieldEditorObserverTest, Paste) {
+ EXPECT_CALL(field_observer_, OnPaste());
+ [editor_ paste:nil];
+}
+
+// Test that -copy: is correctly delegated to the observer.
+TEST_F(AutocompleteTextFieldEditorObserverTest, Copy) {
+ EXPECT_CALL(field_observer_, CanCopy())
+ .WillOnce(Return(true));
+ EXPECT_CALL(field_observer_, CopyToPasteboard(A<NSPasteboard*>()))
+ .Times(1);
+ [editor_ copy:nil];
+}
+
+// Test that -cut: is correctly delegated to the observer and clears
+// the text field.
+TEST_F(AutocompleteTextFieldEditorObserverTest, Cut) {
+ // Sets a string in the field.
+ NSString* test_string = @"astring";
+ EXPECT_CALL(field_observer_, OnDidBeginEditing());
+ EXPECT_CALL(field_observer_, OnDidChange());
+ EXPECT_CALL(field_observer_, SelectionRangeForProposedRange(A<NSRange>()))
+ .WillRepeatedly(ReturnArg<0>());
+ [editor_ setString:test_string];
+ [editor_ selectAll:nil];
+
+ // Calls cut.
+ EXPECT_CALL(field_observer_, CanCopy())
+ .WillOnce(Return(true));
+ EXPECT_CALL(field_observer_, CopyToPasteboard(A<NSPasteboard*>()))
+ .Times(1);
+ [editor_ cut:nil];
+
+ // Check if the field is cleared.
+ ASSERT_EQ([[editor_ textStorage] length], 0U);
+}
+
+// Test that -pasteAndGo: is correctly delegated to the observer.
+TEST_F(AutocompleteTextFieldEditorObserverTest, PasteAndGo) {
+ EXPECT_CALL(field_observer_, OnPasteAndGo());
+ [editor_ pasteAndGo:nil];
+}
+
+// Test that the menu is constructed correctly when CanPasteAndGo().
+TEST_F(AutocompleteTextFieldEditorObserverTest, CanPasteAndGoMenu) {
+ EXPECT_CALL(field_observer_, CanPasteAndGo())
+ .WillOnce(Return(true));
+ EXPECT_CALL(field_observer_, GetPasteActionStringId())
+ .WillOnce(Return(IDS_PASTE_AND_GO));
+
+ NSMenu* menu = [editor_ menuForEvent:nil];
+ NSArray* items = [menu itemArray];
+ ASSERT_EQ([items count], 6U);
+ // TODO(shess): Check the titles, too?
+ NSUInteger i = 0; // Use an index to make future changes easier.
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(cut:));
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(copy:));
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(paste:));
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(pasteAndGo:));
+ EXPECT_TRUE([[items objectAtIndex:i++] isSeparatorItem]);
+
+ EXPECT_EQ([[items objectAtIndex:i] action], @selector(commandDispatch:));
+ EXPECT_EQ([[items objectAtIndex:i] tag], IDC_EDIT_SEARCH_ENGINES);
+ i++;
+}
+
+// Test that the menu is constructed correctly when !CanPasteAndGo().
+TEST_F(AutocompleteTextFieldEditorObserverTest, CannotPasteAndGoMenu) {
+ EXPECT_CALL(field_observer_, CanPasteAndGo())
+ .WillOnce(Return(false));
+
+ NSMenu* menu = [editor_ menuForEvent:nil];
+ NSArray* items = [menu itemArray];
+ ASSERT_EQ([items count], 5U);
+ // TODO(shess): Check the titles, too?
+ NSUInteger i = 0; // Use an index to make future changes easier.
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(cut:));
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(copy:));
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(paste:));
+ EXPECT_TRUE([[items objectAtIndex:i++] isSeparatorItem]);
+
+ EXPECT_EQ([[items objectAtIndex:i] action], @selector(commandDispatch:));
+ EXPECT_EQ([[items objectAtIndex:i] tag], IDC_EDIT_SEARCH_ENGINES);
+ i++;
+}
+
+// Test that the menu is constructed correctly when field isn't
+// editable.
+TEST_F(AutocompleteTextFieldEditorObserverTest, CanPasteAndGoMenuNotEditable) {
+ [field_ setEditable:NO];
+ [editor_ setEditable:NO];
+
+ // Never call these when not editable.
+ EXPECT_CALL(field_observer_, CanPasteAndGo())
+ .Times(0);
+ EXPECT_CALL(field_observer_, GetPasteActionStringId())
+ .Times(0);
+
+ NSMenu* menu = [editor_ menuForEvent:nil];
+ NSArray* items = [menu itemArray];
+ ASSERT_EQ([items count], 3U);
+ // TODO(shess): Check the titles, too?
+ NSUInteger i = 0; // Use an index to make future changes easier.
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(cut:));
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(copy:));
+ EXPECT_EQ([[items objectAtIndex:i++] action], @selector(paste:));
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest.mm b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest.mm
new file mode 100644
index 0000000..14d5476
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest.mm
@@ -0,0 +1,792 @@
+// 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 "app/resource_bundle.h"
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "grit/theme_resources.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+using ::testing::A;
+using ::testing::InSequence;
+using ::testing::Return;
+using ::testing::ReturnArg;
+using ::testing::StrictMock;
+using ::testing::_;
+
+namespace {
+
+class MockDecoration : public LocationBarDecoration {
+ public:
+ virtual CGFloat GetWidthForSpace(CGFloat width) { return 20.0; }
+
+ virtual void DrawInFrame(NSRect frame, NSView* control_view) { ; }
+ MOCK_METHOD0(AcceptsMousePress, bool());
+ MOCK_METHOD1(OnMousePressed, bool(NSRect frame));
+ MOCK_METHOD0(GetMenu, NSMenu*());
+};
+
+// Mock up an incrementing event number.
+NSUInteger eventNumber = 0;
+
+// Create an event of the indicated |type| at |point| within |view|.
+// TODO(shess): Would be nice to have a MockApplication which provided
+// nifty accessors to create these things and inject them. It could
+// even provide functions for "Click and drag mouse from point A to
+// point B".
+NSEvent* Event(NSView* view, const NSPoint point, const NSEventType type,
+ const NSUInteger clickCount) {
+ NSWindow* window([view window]);
+ const NSPoint locationInWindow([view convertPoint:point toView:nil]);
+ const NSPoint location([window convertBaseToScreen:locationInWindow]);
+ return [NSEvent mouseEventWithType:type
+ location:location
+ modifierFlags:0
+ timestamp:0
+ windowNumber:[window windowNumber]
+ context:nil
+ eventNumber:eventNumber++
+ clickCount:clickCount
+ pressure:0.0];
+}
+NSEvent* Event(NSView* view, const NSPoint point, const NSEventType type) {
+ return Event(view, point, type, 1);
+}
+
+// Width of the field so that we don't have to ask |field_| for it all
+// the time.
+static const CGFloat kWidth(300.0);
+
+class AutocompleteTextFieldTest : public CocoaTest {
+ public:
+ AutocompleteTextFieldTest() {
+ // Make sure this is wide enough to play games with the cell
+ // decorations.
+ NSRect frame = NSMakeRect(0, 0, kWidth, 30);
+ scoped_nsobject<AutocompleteTextField> field(
+ [[AutocompleteTextField alloc] initWithFrame:frame]);
+ field_ = field.get();
+ [field_ setStringValue:@"Test test"];
+ [[test_window() contentView] addSubview:field_];
+
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ [cell clearDecorations];
+
+ mock_left_decoration_.SetVisible(false);
+ [cell addLeftDecoration:&mock_left_decoration_];
+
+ mock_right_decoration_.SetVisible(false);
+ [cell addRightDecoration:&mock_right_decoration_];
+
+ window_delegate_.reset(
+ [[AutocompleteTextFieldWindowTestDelegate alloc] init]);
+ [test_window() setDelegate:window_delegate_.get()];
+ }
+
+ NSEvent* KeyDownEventWithFlags(NSUInteger flags) {
+ return [NSEvent keyEventWithType:NSKeyDown
+ location:NSZeroPoint
+ modifierFlags:flags
+ timestamp:0.0
+ windowNumber:[test_window() windowNumber]
+ context:nil
+ characters:@"a"
+ charactersIgnoringModifiers:@"a"
+ isARepeat:NO
+ keyCode:'a'];
+ }
+
+ // Helper to return the field-editor frame being used w/in |field_|.
+ NSRect EditorFrame() {
+ EXPECT_TRUE([field_ currentEditor]);
+ EXPECT_EQ([[field_ subviews] count], 1U);
+ if ([[field_ subviews] count] > 0) {
+ return [[[field_ subviews] objectAtIndex:0] frame];
+ } else {
+ // Return something which won't work so the caller can soldier
+ // on.
+ return NSZeroRect;
+ }
+ }
+
+ AutocompleteTextField* field_;
+ MockDecoration mock_left_decoration_;
+ MockDecoration mock_right_decoration_;
+ scoped_nsobject<AutocompleteTextFieldWindowTestDelegate> window_delegate_;
+};
+
+TEST_VIEW(AutocompleteTextFieldTest, field_);
+
+// Base class for testing AutocompleteTextFieldObserver messages.
+class AutocompleteTextFieldObserverTest : public AutocompleteTextFieldTest {
+ public:
+ virtual void SetUp() {
+ AutocompleteTextFieldTest::SetUp();
+ [field_ setObserver:&field_observer_];
+ }
+
+ virtual void TearDown() {
+ // Clear the observer so that we don't show output for
+ // uninteresting messages to the mock (for instance, if |field_| has
+ // focus at the end of the test).
+ [field_ setObserver:NULL];
+
+ AutocompleteTextFieldTest::TearDown();
+ }
+
+ StrictMock<MockAutocompleteTextFieldObserver> field_observer_;
+};
+
+// Test that we have the right cell class.
+TEST_F(AutocompleteTextFieldTest, CellClass) {
+ EXPECT_TRUE([[field_ cell] isKindOfClass:[AutocompleteTextFieldCell class]]);
+}
+
+// Test that becoming first responder sets things up correctly.
+TEST_F(AutocompleteTextFieldTest, FirstResponder) {
+ EXPECT_EQ(nil, [field_ currentEditor]);
+ EXPECT_EQ([[field_ subviews] count], 0U);
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ EXPECT_FALSE(nil == [field_ currentEditor]);
+ EXPECT_EQ([[field_ subviews] count], 1U);
+ EXPECT_TRUE([[field_ currentEditor] isDescendantOf:field_]);
+
+ // Check that the window delegate is providing the right editor.
+ Class c = [AutocompleteTextFieldEditor class];
+ EXPECT_TRUE([[field_ currentEditor] isKindOfClass:c]);
+}
+
+TEST_F(AutocompleteTextFieldTest, AvailableDecorationWidth) {
+ // A fudge factor to account for how much space the border takes up.
+ // The test shouldn't be too dependent on the field's internals, but
+ // it also shouldn't let deranged cases fall through the cracks
+ // (like nothing available with no text, or everything available
+ // with some text).
+ const CGFloat kBorderWidth = 20.0;
+
+ // With no contents, almost the entire width is available for
+ // decorations.
+ [field_ setStringValue:@""];
+ CGFloat availableWidth = [field_ availableDecorationWidth];
+ EXPECT_LE(availableWidth, kWidth);
+ EXPECT_GT(availableWidth, kWidth - kBorderWidth);
+
+ // With minor contents, most of the remaining width is available for
+ // decorations.
+ NSDictionary* attributes =
+ [NSDictionary dictionaryWithObject:[field_ font]
+ forKey:NSFontAttributeName];
+ NSString* string = @"Hello world";
+ const NSSize size([string sizeWithAttributes:attributes]);
+ [field_ setStringValue:string];
+ availableWidth = [field_ availableDecorationWidth];
+ EXPECT_LE(availableWidth, kWidth - size.width);
+ EXPECT_GT(availableWidth, kWidth - size.width - kBorderWidth);
+
+ // With huge contents, nothing at all is left for decorations.
+ string = @"A long string which is surely wider than field_ can hold.";
+ [field_ setStringValue:string];
+ availableWidth = [field_ availableDecorationWidth];
+ EXPECT_LT(availableWidth, 0.0);
+}
+
+// Test drawing, mostly to ensure nothing leaks or crashes.
+TEST_F(AutocompleteTextFieldTest, Display) {
+ [field_ display];
+
+ // Test focussed drawing.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ [field_ display];
+ [test_window() clearPretendKeyWindowAndFirstResponder];
+}
+
+TEST_F(AutocompleteTextFieldObserverTest, FlagsChanged) {
+ InSequence dummy; // Call mock in exactly the order specified.
+
+ // Test without Control key down, but some other modifier down.
+ EXPECT_CALL(field_observer_, OnControlKeyChanged(false));
+ [field_ flagsChanged:KeyDownEventWithFlags(NSShiftKeyMask)];
+
+ // Test with Control key down.
+ EXPECT_CALL(field_observer_, OnControlKeyChanged(true));
+ [field_ flagsChanged:KeyDownEventWithFlags(NSControlKeyMask)];
+}
+
+// This test is here rather than in the editor's tests because the
+// field catches -flagsChanged: because it's on the responder chain,
+// the field editor doesn't implement it.
+TEST_F(AutocompleteTextFieldObserverTest, FieldEditorFlagsChanged) {
+ // Many of these methods try to change the selection.
+ EXPECT_CALL(field_observer_, SelectionRangeForProposedRange(A<NSRange>()))
+ .WillRepeatedly(ReturnArg<0>());
+
+ InSequence dummy; // Call mock in exactly the order specified.
+ EXPECT_CALL(field_observer_, OnSetFocus(false));
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ NSResponder* firstResponder = [[field_ window] firstResponder];
+ EXPECT_EQ(firstResponder, [field_ currentEditor]);
+
+ // Test without Control key down, but some other modifier down.
+ EXPECT_CALL(field_observer_, OnControlKeyChanged(false));
+ [firstResponder flagsChanged:KeyDownEventWithFlags(NSShiftKeyMask)];
+
+ // Test with Control key down.
+ EXPECT_CALL(field_observer_, OnControlKeyChanged(true));
+ [firstResponder flagsChanged:KeyDownEventWithFlags(NSControlKeyMask)];
+}
+
+// Frame size changes are propagated to |observer_|.
+TEST_F(AutocompleteTextFieldObserverTest, FrameChanged) {
+ EXPECT_CALL(field_observer_, OnFrameChanged());
+ NSRect frame = [field_ frame];
+ frame.size.width += 10.0;
+ [field_ setFrame:frame];
+}
+
+// Test that the field editor gets the same bounds when focus is
+// delivered by the standard focusing machinery, or by
+// -resetFieldEditorFrameIfNeeded.
+TEST_F(AutocompleteTextFieldTest, ResetFieldEditorBase) {
+ // Capture the editor frame resulting from the standard focus
+ // machinery.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ const NSRect baseEditorFrame = EditorFrame();
+
+ // A decoration should result in a strictly smaller editor frame.
+ mock_left_decoration_.SetVisible(true);
+ [field_ resetFieldEditorFrameIfNeeded];
+ EXPECT_FALSE(NSEqualRects(baseEditorFrame, EditorFrame()));
+ EXPECT_TRUE(NSContainsRect(baseEditorFrame, EditorFrame()));
+
+ // Removing the decoration and using -resetFieldEditorFrameIfNeeded
+ // should result in the same frame as the standard focus machinery.
+ mock_left_decoration_.SetVisible(false);
+ [field_ resetFieldEditorFrameIfNeeded];
+ EXPECT_TRUE(NSEqualRects(baseEditorFrame, EditorFrame()));
+}
+
+// Test that the field editor gets the same bounds when focus is
+// delivered by the standard focusing machinery, or by
+// -resetFieldEditorFrameIfNeeded, this time with a decoration
+// pre-loaded.
+TEST_F(AutocompleteTextFieldTest, ResetFieldEditorWithDecoration) {
+ AutocompleteTextFieldCell* cell = [field_ cell];
+
+ // Make sure decoration isn't already visible, then make it visible.
+ EXPECT_TRUE(NSIsEmptyRect([cell frameForDecoration:&mock_left_decoration_
+ inFrame:[field_ bounds]]));
+ mock_left_decoration_.SetVisible(true);
+ EXPECT_FALSE(NSIsEmptyRect([cell frameForDecoration:&mock_left_decoration_
+ inFrame:[field_ bounds]]));
+
+ // Capture the editor frame resulting from the standard focus
+ // machinery.
+
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ const NSRect baseEditorFrame = EditorFrame();
+
+ // When the decoration is not visible the frame should be strictly larger.
+ mock_left_decoration_.SetVisible(false);
+ EXPECT_TRUE(NSIsEmptyRect([cell frameForDecoration:&mock_left_decoration_
+ inFrame:[field_ bounds]]));
+ [field_ resetFieldEditorFrameIfNeeded];
+ EXPECT_FALSE(NSEqualRects(baseEditorFrame, EditorFrame()));
+ EXPECT_TRUE(NSContainsRect(EditorFrame(), baseEditorFrame));
+
+ // When the decoration is visible, -resetFieldEditorFrameIfNeeded
+ // should result in the same frame as the standard focus machinery.
+ mock_left_decoration_.SetVisible(true);
+ EXPECT_FALSE(NSIsEmptyRect([cell frameForDecoration:&mock_left_decoration_
+ inFrame:[field_ bounds]]));
+
+ [field_ resetFieldEditorFrameIfNeeded];
+ EXPECT_TRUE(NSEqualRects(baseEditorFrame, EditorFrame()));
+}
+
+// Test that resetting the field editor bounds does not cause untoward
+// messages to the field's observer.
+TEST_F(AutocompleteTextFieldObserverTest, ResetFieldEditorContinuesEditing) {
+ // Many of these methods try to change the selection.
+ EXPECT_CALL(field_observer_, SelectionRangeForProposedRange(A<NSRange>()))
+ .WillRepeatedly(ReturnArg<0>());
+
+ EXPECT_CALL(field_observer_, OnSetFocus(false));
+ // Becoming first responder doesn't begin editing.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ const NSRect baseEditorFrame = EditorFrame();
+ NSTextView* editor = static_cast<NSTextView*>([field_ currentEditor]);
+ EXPECT_TRUE(nil != editor);
+
+ // This should begin editing and indicate a change.
+ EXPECT_CALL(field_observer_, OnDidBeginEditing());
+ EXPECT_CALL(field_observer_, OnDidChange());
+ [editor shouldChangeTextInRange:NSMakeRange(0, 0) replacementString:@""];
+ [editor didChangeText];
+
+ // No messages to |field_observer_| when the frame actually changes.
+ mock_left_decoration_.SetVisible(true);
+ [field_ resetFieldEditorFrameIfNeeded];
+ EXPECT_FALSE(NSEqualRects(baseEditorFrame, EditorFrame()));
+}
+
+// Clicking in a right-hand decoration which does not handle the mouse
+// puts the caret rightmost.
+TEST_F(AutocompleteTextFieldTest, ClickRightDecorationPutsCaretRightmost) {
+ // Decoration does not handle the mouse event, so the cell should
+ // process it. Called at least once.
+ EXPECT_CALL(mock_right_decoration_, AcceptsMousePress())
+ .WillOnce(Return(false))
+ .WillRepeatedly(Return(false));
+
+ // Set the decoration before becoming responder.
+ EXPECT_FALSE([field_ currentEditor]);
+ mock_right_decoration_.SetVisible(true);
+
+ // Make first responder should select all.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ EXPECT_TRUE([field_ currentEditor]);
+ const NSRange allRange = NSMakeRange(0, [[field_ stringValue] length]);
+ EXPECT_TRUE(NSEqualRanges(allRange, [[field_ currentEditor] selectedRange]));
+
+ // Generate a click on the decoration.
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ const NSRect bounds = [field_ bounds];
+ const NSRect iconFrame =
+ [cell frameForDecoration:&mock_right_decoration_ inFrame:bounds];
+ const NSPoint point = NSMakePoint(NSMidX(iconFrame), NSMidY(iconFrame));
+ NSEvent* downEvent = Event(field_, point, NSLeftMouseDown);
+ NSEvent* upEvent = Event(field_, point, NSLeftMouseUp);
+ [NSApp postEvent:upEvent atStart:YES];
+ [field_ mouseDown:downEvent];
+
+ // Selection should be a right-hand-side caret.
+ EXPECT_TRUE(NSEqualRanges(NSMakeRange([[field_ stringValue] length], 0),
+ [[field_ currentEditor] selectedRange]));
+}
+
+// Clicking in a left-side decoration which doesn't handle the event
+// puts the selection in the leftmost position.
+TEST_F(AutocompleteTextFieldTest, ClickLeftDecorationPutsCaretLeftmost) {
+ // Decoration does not handle the mouse event, so the cell should
+ // process it. Called at least once.
+ EXPECT_CALL(mock_left_decoration_, AcceptsMousePress())
+ .WillOnce(Return(false))
+ .WillRepeatedly(Return(false));
+
+ // Set the decoration before becoming responder.
+ EXPECT_FALSE([field_ currentEditor]);
+ mock_left_decoration_.SetVisible(true);
+
+ // Make first responder should select all.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ EXPECT_TRUE([field_ currentEditor]);
+ const NSRange allRange = NSMakeRange(0, [[field_ stringValue] length]);
+ EXPECT_TRUE(NSEqualRanges(allRange, [[field_ currentEditor] selectedRange]));
+
+ // Generate a click on the decoration.
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ const NSRect bounds = [field_ bounds];
+ const NSRect iconFrame =
+ [cell frameForDecoration:&mock_left_decoration_ inFrame:bounds];
+ const NSPoint point = NSMakePoint(NSMidX(iconFrame), NSMidY(iconFrame));
+ NSEvent* downEvent = Event(field_, point, NSLeftMouseDown);
+ NSEvent* upEvent = Event(field_, point, NSLeftMouseUp);
+ [NSApp postEvent:upEvent atStart:YES];
+ [field_ mouseDown:downEvent];
+
+ // Selection should be a left-hand-side caret.
+ EXPECT_TRUE(NSEqualRanges(NSMakeRange(0, 0),
+ [[field_ currentEditor] selectedRange]));
+}
+
+// Clicks not in the text area or the cell's decorations fall through
+// to the editor.
+TEST_F(AutocompleteTextFieldTest, ClickBorderSelectsAll) {
+ // Can't rely on the window machinery to make us first responder,
+ // here.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ EXPECT_TRUE([field_ currentEditor]);
+
+ const NSPoint point(NSMakePoint(20.0, 1.0));
+ NSEvent* downEvent(Event(field_, point, NSLeftMouseDown));
+ NSEvent* upEvent(Event(field_, point, NSLeftMouseUp));
+ [NSApp postEvent:upEvent atStart:YES];
+ [field_ mouseDown:downEvent];
+
+ // Clicking in the narrow border area around a Cocoa NSTextField
+ // does a select-all. Regardless of whether this is a good call, it
+ // works as a test that things get passed down to the editor.
+ const NSRange selectedRange([[field_ currentEditor] selectedRange]);
+ EXPECT_EQ(selectedRange.location, 0U);
+ EXPECT_EQ(selectedRange.length, [[field_ stringValue] length]);
+}
+
+// Single-click with no drag should setup a field editor and
+// select all.
+TEST_F(AutocompleteTextFieldTest, ClickSelectsAll) {
+ EXPECT_FALSE([field_ currentEditor]);
+
+ const NSPoint point = NSMakePoint(20.0, NSMidY([field_ bounds]));
+ NSEvent* downEvent(Event(field_, point, NSLeftMouseDown));
+ NSEvent* upEvent(Event(field_, point, NSLeftMouseUp));
+ [NSApp postEvent:upEvent atStart:YES];
+ [field_ mouseDown:downEvent];
+ EXPECT_TRUE([field_ currentEditor]);
+ const NSRange selectedRange([[field_ currentEditor] selectedRange]);
+ EXPECT_EQ(selectedRange.location, 0U);
+ EXPECT_EQ(selectedRange.length, [[field_ stringValue] length]);
+}
+
+// Click-drag selects text, not select all.
+TEST_F(AutocompleteTextFieldTest, ClickDragSelectsText) {
+ EXPECT_FALSE([field_ currentEditor]);
+
+ NSEvent* downEvent(Event(field_, NSMakePoint(20.0, 5.0), NSLeftMouseDown));
+ NSEvent* upEvent(Event(field_, NSMakePoint(0.0, 5.0), NSLeftMouseUp));
+ [NSApp postEvent:upEvent atStart:YES];
+ [field_ mouseDown:downEvent];
+ EXPECT_TRUE([field_ currentEditor]);
+
+ // Expect this to have selected a prefix of the content. Mostly
+ // just don't want the select-all behavior.
+ const NSRange selectedRange([[field_ currentEditor] selectedRange]);
+ EXPECT_EQ(selectedRange.location, 0U);
+ EXPECT_LT(selectedRange.length, [[field_ stringValue] length]);
+}
+
+// TODO(shess): Test that click/pause/click allows cursor placement.
+// In this case the first click goes to the field, but the second
+// click goes to the field editor, so the current testing pattern
+// can't work. What really needs to happen is to push through the
+// NSWindow event machinery so that we can say "two independent clicks
+// at the same location have the right effect". Once that is done, it
+// might make sense to revise the other tests to use the same
+// machinery.
+
+// Double-click selects word, not select all.
+TEST_F(AutocompleteTextFieldTest, DoubleClickSelectsWord) {
+ EXPECT_FALSE([field_ currentEditor]);
+
+ const NSPoint point = NSMakePoint(20.0, NSMidY([field_ bounds]));
+ NSEvent* downEvent(Event(field_, point, NSLeftMouseDown, 1));
+ NSEvent* upEvent(Event(field_, point, NSLeftMouseUp, 1));
+ NSEvent* downEvent2(Event(field_, point, NSLeftMouseDown, 2));
+ NSEvent* upEvent2(Event(field_, point, NSLeftMouseUp, 2));
+ [NSApp postEvent:upEvent atStart:YES];
+ [field_ mouseDown:downEvent];
+ [NSApp postEvent:upEvent2 atStart:YES];
+ [field_ mouseDown:downEvent2];
+ EXPECT_TRUE([field_ currentEditor]);
+
+ // Selected the first word.
+ const NSRange selectedRange([[field_ currentEditor] selectedRange]);
+ const NSRange spaceRange([[field_ stringValue] rangeOfString:@" "]);
+ EXPECT_GT(spaceRange.location, 0U);
+ EXPECT_LT(spaceRange.length, [[field_ stringValue] length]);
+ EXPECT_EQ(selectedRange.location, 0U);
+ EXPECT_EQ(selectedRange.length, spaceRange.location);
+}
+
+TEST_F(AutocompleteTextFieldTest, TripleClickSelectsAll) {
+ EXPECT_FALSE([field_ currentEditor]);
+
+ const NSPoint point(NSMakePoint(20.0, 5.0));
+ NSEvent* downEvent(Event(field_, point, NSLeftMouseDown, 1));
+ NSEvent* upEvent(Event(field_, point, NSLeftMouseUp, 1));
+ NSEvent* downEvent2(Event(field_, point, NSLeftMouseDown, 2));
+ NSEvent* upEvent2(Event(field_, point, NSLeftMouseUp, 2));
+ NSEvent* downEvent3(Event(field_, point, NSLeftMouseDown, 3));
+ NSEvent* upEvent3(Event(field_, point, NSLeftMouseUp, 3));
+ [NSApp postEvent:upEvent atStart:YES];
+ [field_ mouseDown:downEvent];
+ [NSApp postEvent:upEvent2 atStart:YES];
+ [field_ mouseDown:downEvent2];
+ [NSApp postEvent:upEvent3 atStart:YES];
+ [field_ mouseDown:downEvent3];
+ EXPECT_TRUE([field_ currentEditor]);
+
+ // Selected the first word.
+ const NSRange selectedRange([[field_ currentEditor] selectedRange]);
+ EXPECT_EQ(selectedRange.location, 0U);
+ EXPECT_EQ(selectedRange.length, [[field_ stringValue] length]);
+}
+
+// Clicking a decoration should call decoration's OnMousePressed.
+TEST_F(AutocompleteTextFieldTest, LeftDecorationMouseDown) {
+ // At this point, not focussed.
+ EXPECT_FALSE([field_ currentEditor]);
+
+ mock_left_decoration_.SetVisible(true);
+ EXPECT_CALL(mock_left_decoration_, AcceptsMousePress())
+ .WillRepeatedly(Return(true));
+
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ const NSRect iconFrame =
+ [cell frameForDecoration:&mock_left_decoration_ inFrame:[field_ bounds]];
+ const NSPoint location = NSMakePoint(NSMidX(iconFrame), NSMidY(iconFrame));
+ NSEvent* downEvent = Event(field_, location, NSLeftMouseDown, 1);
+ NSEvent* upEvent = Event(field_, location, NSLeftMouseUp, 1);
+
+ // Since decorations can be dragged, the mouse-press is sent on
+ // mouse-up.
+ [NSApp postEvent:upEvent atStart:YES];
+
+ EXPECT_CALL(mock_left_decoration_, OnMousePressed(_))
+ .WillOnce(Return(true));
+ [field_ mouseDown:downEvent];
+
+ // Focus the field and test that handled clicks don't affect selection.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ EXPECT_TRUE([field_ currentEditor]);
+ const NSRange allRange = NSMakeRange(0, [[field_ stringValue] length]);
+ EXPECT_TRUE(NSEqualRanges(allRange, [[field_ currentEditor] selectedRange]));
+
+ // Generate another click on the decoration.
+ downEvent = Event(field_, location, NSLeftMouseDown, 1);
+ upEvent = Event(field_, location, NSLeftMouseUp, 1);
+ [NSApp postEvent:upEvent atStart:YES];
+ EXPECT_CALL(mock_left_decoration_, OnMousePressed(_))
+ .WillOnce(Return(true));
+ [field_ mouseDown:downEvent];
+
+ // The selection should not have changed.
+ EXPECT_TRUE(NSEqualRanges(allRange, [[field_ currentEditor] selectedRange]));
+
+ // TODO(shess): Test that mouse drags are initiated if the next
+ // event is a drag, or if the mouse-up takes too long to arrive.
+ // IDEA: mock decoration to return a pasteboard which a mock
+ // AutocompleteTextField notes in -dragImage:*.
+}
+
+// Clicking a decoration should call decoration's OnMousePressed.
+TEST_F(AutocompleteTextFieldTest, RightDecorationMouseDown) {
+ // At this point, not focussed.
+ EXPECT_FALSE([field_ currentEditor]);
+
+ mock_right_decoration_.SetVisible(true);
+ EXPECT_CALL(mock_right_decoration_, AcceptsMousePress())
+ .WillRepeatedly(Return(true));
+
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ const NSRect bounds = [field_ bounds];
+ const NSRect iconFrame =
+ [cell frameForDecoration:&mock_right_decoration_ inFrame:bounds];
+ const NSPoint location = NSMakePoint(NSMidX(iconFrame), NSMidY(iconFrame));
+ NSEvent* downEvent = Event(field_, location, NSLeftMouseDown, 1);
+ NSEvent* upEvent = Event(field_, location, NSLeftMouseUp, 1);
+
+ // Since decorations can be dragged, the mouse-press is sent on
+ // mouse-up.
+ [NSApp postEvent:upEvent atStart:YES];
+
+ EXPECT_CALL(mock_right_decoration_, OnMousePressed(_))
+ .WillOnce(Return(true));
+ [field_ mouseDown:downEvent];
+}
+
+// Test that page action menus are properly returned.
+// TODO(shess): Really, this should test that things are forwarded to
+// the cell, and the cell tests should test that the right things are
+// selected. It's easier to mock the event here, though. This code's
+// event-mockers might be worth promoting to |test_event_utils.h| or
+// |cocoa_test_helper.h|.
+TEST_F(AutocompleteTextFieldTest, DecorationMenu) {
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ const NSRect bounds([field_ bounds]);
+
+ const CGFloat edge = NSHeight(bounds) - 4.0;
+ const NSSize size = NSMakeSize(edge, edge);
+ scoped_nsobject<NSImage> image([[NSImage alloc] initWithSize:size]);
+
+ scoped_nsobject<NSMenu> menu([[NSMenu alloc] initWithTitle:@"Menu"]);
+
+ mock_left_decoration_.SetVisible(true);
+ mock_right_decoration_.SetVisible(true);
+
+ // The item with a menu returns it.
+ NSRect actionFrame = [cell frameForDecoration:&mock_right_decoration_
+ inFrame:bounds];
+ NSPoint location = NSMakePoint(NSMidX(actionFrame), NSMidY(actionFrame));
+ NSEvent* event = Event(field_, location, NSRightMouseDown, 1);
+
+ // Check that the decoration is called, and the field returns the
+ // menu.
+ EXPECT_CALL(mock_right_decoration_, GetMenu())
+ .WillOnce(Return(menu.get()));
+ NSMenu *decorationMenu = [field_ decorationMenuForEvent:event];
+ EXPECT_EQ(decorationMenu, menu);
+
+ // The item without a menu returns nil.
+ EXPECT_CALL(mock_left_decoration_, GetMenu())
+ .WillOnce(Return(static_cast<NSMenu*>(nil)));
+ actionFrame = [cell frameForDecoration:&mock_left_decoration_
+ inFrame:bounds];
+ location = NSMakePoint(NSMidX(actionFrame), NSMidY(actionFrame));
+ event = Event(field_, location, NSRightMouseDown, 1);
+ EXPECT_FALSE([field_ decorationMenuForEvent:event]);
+
+ // Something not in an action returns nil.
+ location = NSMakePoint(NSMidX(bounds), NSMidY(bounds));
+ event = Event(field_, location, NSRightMouseDown, 1);
+ EXPECT_FALSE([field_ decorationMenuForEvent:event]);
+}
+
+// Verify that -setAttributedStringValue: works as expected when
+// focussed or when not focussed. Our code mostly depends on about
+// whether -stringValue works right.
+TEST_F(AutocompleteTextFieldTest, SetAttributedStringBaseline) {
+ EXPECT_EQ(nil, [field_ currentEditor]);
+
+ // So that we can set rich text.
+ [field_ setAllowsEditingTextAttributes:YES];
+
+ // Set an attribute different from the field's default so we can
+ // tell we got the same string out as we put in.
+ NSFont* font = [NSFont fontWithDescriptor:[[field_ font] fontDescriptor]
+ size:[[field_ font] pointSize] + 2];
+ NSDictionary* attributes =
+ [NSDictionary dictionaryWithObject:font
+ forKey:NSFontAttributeName];
+ NSString* const kString = @"This is a test";
+ scoped_nsobject<NSAttributedString> attributedString(
+ [[NSAttributedString alloc] initWithString:kString
+ attributes:attributes]);
+
+ // Check that what we get back looks like what we put in.
+ EXPECT_NSNE(kString, [field_ stringValue]);
+ [field_ setAttributedStringValue:attributedString];
+ EXPECT_TRUE([[field_ attributedStringValue]
+ isEqualToAttributedString:attributedString]);
+ EXPECT_NSEQ(kString, [field_ stringValue]);
+
+ // Try that again with focus.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+
+ EXPECT_TRUE([field_ currentEditor]);
+
+ // Check that what we get back looks like what we put in.
+ [field_ setStringValue:@""];
+ EXPECT_NSNE(kString, [field_ stringValue]);
+ [field_ setAttributedStringValue:attributedString];
+ EXPECT_TRUE([[field_ attributedStringValue]
+ isEqualToAttributedString:attributedString]);
+ EXPECT_NSEQ(kString, [field_ stringValue]);
+}
+
+// -setAttributedStringValue: shouldn't reset the undo state if things
+// are being editted.
+TEST_F(AutocompleteTextFieldTest, SetAttributedStringUndo) {
+ NSColor* redColor = [NSColor redColor];
+ NSDictionary* attributes =
+ [NSDictionary dictionaryWithObject:redColor
+ forKey:NSForegroundColorAttributeName];
+ NSString* const kString = @"This is a test";
+ scoped_nsobject<NSAttributedString> attributedString(
+ [[NSAttributedString alloc] initWithString:kString
+ attributes:attributes]);
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ EXPECT_TRUE([field_ currentEditor]);
+ NSTextView* editor = static_cast<NSTextView*>([field_ currentEditor]);
+ NSUndoManager* undoManager = [editor undoManager];
+ EXPECT_TRUE(undoManager);
+
+ // Nothing to undo, yet.
+ EXPECT_FALSE([undoManager canUndo]);
+
+ // Starting an editing action creates an undoable item.
+ [editor shouldChangeTextInRange:NSMakeRange(0, 0) replacementString:@""];
+ [editor didChangeText];
+ EXPECT_TRUE([undoManager canUndo]);
+
+ // -setStringValue: resets the editor's undo chain.
+ [field_ setStringValue:kString];
+ EXPECT_FALSE([undoManager canUndo]);
+
+ // Verify that -setAttributedStringValue: does not reset the
+ // editor's undo chain.
+ [field_ setStringValue:@""];
+ [editor shouldChangeTextInRange:NSMakeRange(0, 0) replacementString:@""];
+ [editor didChangeText];
+ EXPECT_TRUE([undoManager canUndo]);
+ [field_ setAttributedStringValue:attributedString];
+ EXPECT_TRUE([undoManager canUndo]);
+
+ // Verify that calling -clearUndoChain clears the undo chain.
+ [field_ clearUndoChain];
+ EXPECT_FALSE([undoManager canUndo]);
+}
+
+TEST_F(AutocompleteTextFieldTest, EditorGetsCorrectUndoManager) {
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+
+ NSTextView* editor = static_cast<NSTextView*>([field_ currentEditor]);
+ EXPECT_TRUE(editor);
+ EXPECT_EQ([field_ undoManagerForTextView:editor], [editor undoManager]);
+}
+
+TEST_F(AutocompleteTextFieldObserverTest, SendsEditingMessages) {
+ // Many of these methods try to change the selection.
+ EXPECT_CALL(field_observer_, SelectionRangeForProposedRange(A<NSRange>()))
+ .WillRepeatedly(ReturnArg<0>());
+
+ EXPECT_CALL(field_observer_, OnSetFocus(false));
+ // Becoming first responder doesn't begin editing.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ NSTextView* editor = static_cast<NSTextView*>([field_ currentEditor]);
+ EXPECT_TRUE(nil != editor);
+
+ // This should begin editing and indicate a change.
+ EXPECT_CALL(field_observer_, OnDidBeginEditing());
+ EXPECT_CALL(field_observer_, OnDidChange());
+ [editor shouldChangeTextInRange:NSMakeRange(0, 0) replacementString:@""];
+ [editor didChangeText];
+
+ // Further changes don't send the begin message.
+ EXPECT_CALL(field_observer_, OnDidChange());
+ [editor shouldChangeTextInRange:NSMakeRange(0, 0) replacementString:@""];
+ [editor didChangeText];
+
+ // -doCommandBySelector: should forward to observer via |field_|.
+ // TODO(shess): Test with a fake arrow-key event?
+ const SEL cmd = @selector(moveDown:);
+ EXPECT_CALL(field_observer_, OnDoCommandBySelector(cmd))
+ .WillOnce(Return(true));
+ [editor doCommandBySelector:cmd];
+
+ // Finished with the changes.
+ EXPECT_CALL(field_observer_, OnKillFocus());
+ EXPECT_CALL(field_observer_, OnDidEndEditing());
+ [test_window() clearPretendKeyWindowAndFirstResponder];
+}
+
+// Test that the resign-key notification is forwarded right, and that
+// the notification is registered and unregistered when the view moves
+// in and out of the window.
+// TODO(shess): Should this test the key window for realz? That would
+// be really annoying to whoever is running the tests.
+TEST_F(AutocompleteTextFieldObserverTest, ClosePopupOnResignKey) {
+ EXPECT_CALL(field_observer_, ClosePopup());
+ [test_window() resignKeyWindow];
+
+ scoped_nsobject<AutocompleteTextField> pin([field_ retain]);
+ [field_ removeFromSuperview];
+ [test_window() resignKeyWindow];
+
+ [[test_window() contentView] addSubview:field_];
+ EXPECT_CALL(field_observer_, ClosePopup());
+ [test_window() resignKeyWindow];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.h b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.h
new file mode 100644
index 0000000..bccaae1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.h
@@ -0,0 +1,58 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_AUTOCOMPLETE_TEXT_FIELD_UNITTEST_HELPER_H_
+#define CHROME_BROWSER_UI_COCOA_AUTOCOMPLETE_TEXT_FIELD_UNITTEST_HELPER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h"
+#include "testing/gmock/include/gmock/gmock.h"
+
+@class AutocompleteTextFieldEditor;
+
+// Return the right field editor for AutocompleteTextField instance.
+
+@interface AutocompleteTextFieldWindowTestDelegate :
+ NSObject<NSWindowDelegate> {
+ scoped_nsobject<AutocompleteTextFieldEditor> editor_;
+}
+- (id)windowWillReturnFieldEditor:(NSWindow *)sender toObject:(id)anObject;
+@end
+
+namespace {
+
+// Allow monitoring calls into AutocompleteTextField's observer.
+// Being in a .h file with an anonymous namespace is strange, but this
+// is here so the mock interface doesn't have to change in multiple
+// places.
+
+// Any method you add here needs a unit test. You knew that.
+
+class MockAutocompleteTextFieldObserver : public AutocompleteTextFieldObserver {
+ public:
+ MOCK_METHOD1(SelectionRangeForProposedRange, NSRange(NSRange range));
+ MOCK_METHOD1(OnControlKeyChanged, void(bool pressed));
+ MOCK_METHOD0(CanCopy, bool());
+ MOCK_METHOD1(CopyToPasteboard, void(NSPasteboard* pboard));
+ MOCK_METHOD0(OnPaste, void());
+ MOCK_METHOD0(CanPasteAndGo, bool());
+ MOCK_METHOD0(GetPasteActionStringId, int());
+ MOCK_METHOD0(OnPasteAndGo, void());
+ MOCK_METHOD0(OnFrameChanged, void());
+ MOCK_METHOD0(ClosePopup, void());
+ MOCK_METHOD0(OnDidBeginEditing, void());
+ MOCK_METHOD0(OnDidChange, void());
+ MOCK_METHOD0(OnDidEndEditing, void());
+ MOCK_METHOD1(OnDoCommandBySelector, bool(SEL cmd));
+ MOCK_METHOD1(OnSetFocus, void(bool control_down));
+ MOCK_METHOD0(OnKillFocus, void());
+};
+
+} // namespace
+
+#endif // CHROME_BROWSER_UI_COCOA_AUTOCOMPLETE_TEXT_FIELD_UNITTEST_HELPER_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.mm b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.mm
new file mode 100644
index 0000000..a2c5194
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.mm
@@ -0,0 +1,29 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_unittest_helper.h"
+
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+@implementation AutocompleteTextFieldWindowTestDelegate
+
+- (id)windowWillReturnFieldEditor:(NSWindow *)sender toObject:(id)anObject {
+ id editor = nil;
+ if ([anObject isKindOfClass:[AutocompleteTextField class]]) {
+ if (editor_ == nil) {
+ editor_.reset([[AutocompleteTextFieldEditor alloc] init]);
+ }
+ EXPECT_TRUE(editor_ != nil);
+
+ // This needs to be called every time, otherwise notifications
+ // aren't sent correctly.
+ [editor_ setFieldEditor:YES];
+ editor = editor_.get();
+ }
+ return editor;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/bubble_decoration.h b/chrome/browser/ui/cocoa/location_bar/bubble_decoration.h
new file mode 100644
index 0000000..234c254
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/bubble_decoration.h
@@ -0,0 +1,67 @@
+// 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_UI_COCOA_LOCATION_BAR_BUBBLE_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_BUBBLE_DECORATION_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/gtest_prod_util.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h"
+
+// Draws an outlined rounded rect, with an optional image to the left
+// and an optional text label to the right.
+
+class BubbleDecoration : public LocationBarDecoration {
+ public:
+ // |font| will be used when drawing the label, and cannot be |nil|.
+ BubbleDecoration(NSFont* font);
+ ~BubbleDecoration();
+
+ // Setup the drawing parameters.
+ NSImage* GetImage();
+ void SetImage(NSImage* image);
+ void SetLabel(NSString* label);
+ void SetColors(NSColor* border_color,
+ NSColor* background_color,
+ NSColor* text_color);
+
+ // Implement |LocationBarDecoration|.
+ virtual void DrawInFrame(NSRect frame, NSView* control_view);
+ virtual CGFloat GetWidthForSpace(CGFloat width);
+
+ protected:
+ // Helper returning bubble width for the given |image| and |label|
+ // assuming |font_| (for sizing text). Arguments can be nil.
+ CGFloat GetWidthForImageAndLabel(NSImage* image, NSString* label);
+
+ // Helper to return where the image is drawn, for subclasses to drag
+ // from. |frame| is the decoration's frame in the containing cell.
+ NSRect GetImageRectInFrame(NSRect frame);
+
+ private:
+ friend class SelectedKeywordDecorationTest;
+ FRIEND_TEST_ALL_PREFIXES(SelectedKeywordDecorationTest,
+ UsesPartialKeywordIfNarrow);
+
+ // Contains font and color attribute for drawing |label_|.
+ scoped_nsobject<NSDictionary> attributes_;
+
+ // Image drawn in the left side of the bubble.
+ scoped_nsobject<NSImage> image_;
+
+ // Label to draw to right of image. Can be |nil|.
+ scoped_nsobject<NSString> label_;
+
+ // Colors used to draw the bubble, should be set by the subclass
+ // constructor.
+ scoped_nsobject<NSColor> background_color_;
+ scoped_nsobject<NSColor> border_color_;
+
+ DISALLOW_COPY_AND_ASSIGN(BubbleDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_BUBBLE_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/bubble_decoration.mm b/chrome/browser/ui/cocoa/location_bar/bubble_decoration.mm
new file mode 100644
index 0000000..b568639
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/bubble_decoration.mm
@@ -0,0 +1,158 @@
+// 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 <cmath>
+
+#import "chrome/browser/ui/cocoa/location_bar/bubble_decoration.h"
+
+#include "base/logging.h"
+#import "chrome/browser/ui/cocoa/image_utils.h"
+
+namespace {
+
+// Padding between the icon/label and bubble edges.
+const CGFloat kBubblePadding = 3.0;
+
+// The image needs to be in the same position as for the location
+// icon, which implies that the bubble's padding in the Omnibox needs
+// to differ from the location icon's. Indeed, that's how the views
+// implementation handles the problem. This draws the bubble edge a
+// little bit further left, which is easier but no less hacky.
+const CGFloat kLeftSideOverdraw = 2.0;
+
+// Omnibox corner radius is |4.0|, this needs to look tight WRT that.
+const CGFloat kBubbleCornerRadius = 2.0;
+
+// How far to inset the bubble from the top and bottom of the drawing
+// frame.
+// TODO(shess): Would be nicer to have the drawing code factor out the
+// space outside the border, and perhaps the border. Then this could
+// reflect the single pixel space w/in that.
+const CGFloat kBubbleYInset = 4.0;
+
+} // namespace
+
+BubbleDecoration::BubbleDecoration(NSFont* font) {
+ DCHECK(font);
+ if (font) {
+ NSDictionary* attributes =
+ [NSDictionary dictionaryWithObject:font
+ forKey:NSFontAttributeName];
+ attributes_.reset([attributes retain]);
+ }
+}
+
+BubbleDecoration::~BubbleDecoration() {
+}
+
+CGFloat BubbleDecoration::GetWidthForImageAndLabel(NSImage* image,
+ NSString* label) {
+ if (!image && !label)
+ return kOmittedWidth;
+
+ const CGFloat image_width = image ? [image size].width : 0.0;
+ if (!label)
+ return kBubblePadding + image_width;
+
+ // The bubble needs to take up an integral number of pixels.
+ // Generally -sizeWithAttributes: seems to overestimate rather than
+ // underestimate, so floor() seems to work better.
+ const CGFloat label_width =
+ std::floor([label sizeWithAttributes:attributes_].width);
+ return kBubblePadding + image_width + label_width;
+}
+
+NSRect BubbleDecoration::GetImageRectInFrame(NSRect frame) {
+ NSRect imageRect = NSInsetRect(frame, 0.0, kBubbleYInset);
+ if (image_) {
+ // Center the image vertically.
+ const NSSize imageSize = [image_ size];
+ imageRect.origin.y +=
+ std::floor((NSHeight(frame) - imageSize.height) / 2.0);
+ imageRect.size = imageSize;
+ }
+ return imageRect;
+}
+
+CGFloat BubbleDecoration::GetWidthForSpace(CGFloat width) {
+ const CGFloat all_width = GetWidthForImageAndLabel(image_, label_);
+ if (all_width <= width)
+ return all_width;
+
+ const CGFloat image_width = GetWidthForImageAndLabel(image_, nil);
+ if (image_width <= width)
+ return image_width;
+
+ return kOmittedWidth;
+}
+
+void BubbleDecoration::DrawInFrame(NSRect frame, NSView* control_view) {
+ const NSRect decorationFrame = NSInsetRect(frame, 0.0, kBubbleYInset);
+
+ // The inset is to put the border down the middle of the pixel.
+ NSRect bubbleFrame = NSInsetRect(decorationFrame, 0.5, 0.5);
+ bubbleFrame.origin.x -= kLeftSideOverdraw;
+ bubbleFrame.size.width += kLeftSideOverdraw;
+ NSBezierPath* path =
+ [NSBezierPath bezierPathWithRoundedRect:bubbleFrame
+ xRadius:kBubbleCornerRadius
+ yRadius:kBubbleCornerRadius];
+
+ [background_color_ setFill];
+ [path fill];
+
+ [border_color_ setStroke];
+ [path setLineWidth:1.0];
+ [path stroke];
+
+ NSRect imageRect = decorationFrame;
+ if (image_) {
+ // Center the image vertically.
+ const NSSize imageSize = [image_ size];
+ imageRect.origin.y +=
+ std::floor((NSHeight(decorationFrame) - imageSize.height) / 2.0);
+ imageRect.size = imageSize;
+ [image_ drawInRect:imageRect
+ fromRect:NSZeroRect // Entire image
+ operation:NSCompositeSourceOver
+ fraction:1.0
+ neverFlipped:YES];
+ } else {
+ imageRect.size = NSZeroSize;
+ }
+
+ if (label_) {
+ NSRect textRect = decorationFrame;
+ textRect.origin.x = NSMaxX(imageRect);
+ textRect.size.width = NSMaxX(decorationFrame) - NSMinX(textRect);
+ [label_ drawInRect:textRect withAttributes:attributes_];
+ }
+}
+
+NSImage* BubbleDecoration::GetImage() {
+ return image_;
+}
+
+void BubbleDecoration::SetImage(NSImage* image) {
+ image_.reset([image retain]);
+}
+
+void BubbleDecoration::SetLabel(NSString* label) {
+ // If the initializer was called with |nil|, then the code cannot
+ // process a label.
+ DCHECK(attributes_);
+ if (attributes_)
+ label_.reset([label copy]);
+}
+
+void BubbleDecoration::SetColors(NSColor* border_color,
+ NSColor* background_color,
+ NSColor* text_color) {
+ border_color_.reset([border_color retain]);
+ background_color_.reset([background_color retain]);
+
+ scoped_nsobject<NSMutableDictionary> attributes([attributes_ mutableCopy]);
+ [attributes setObject:text_color forKey:NSForegroundColorAttributeName];
+ attributes_.reset([attributes copy]);
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/content_setting_decoration.h b/chrome/browser/ui/cocoa/location_bar/content_setting_decoration.h
new file mode 100644
index 0000000..07b1510
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/content_setting_decoration.h
@@ -0,0 +1,55 @@
+// 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_UI_COCOA_LOCATION_BAR_CONTENT_SETTING_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_CONTENT_SETTING_DECORATION_H_
+#pragma once
+
+#include "base/scoped_ptr.h"
+#import "chrome/browser/ui/cocoa/location_bar/image_decoration.h"
+#include "chrome/common/content_settings_types.h"
+
+// ContentSettingDecoration is used to display the content settings
+// images on the current page.
+
+class ContentSettingImageModel;
+class LocationBarViewMac;
+class Profile;
+class TabContents;
+
+class ContentSettingDecoration : public ImageDecoration {
+ public:
+ ContentSettingDecoration(ContentSettingsType settings_type,
+ LocationBarViewMac* owner,
+ Profile* profile);
+ virtual ~ContentSettingDecoration();
+
+ // Updates the image and visibility state based on the supplied TabContents.
+ // Returns true if the decoration's visible state changed.
+ bool UpdateFromTabContents(TabContents* tab_contents);
+
+ // Overridden from |LocationBarDecoration|
+ virtual bool AcceptsMousePress() { return true; }
+ virtual bool OnMousePressed(NSRect frame);
+ virtual NSString* GetToolTip();
+
+ private:
+ // Helper to get where the bubble point should land. Similar to
+ // |PageActionDecoration| or |StarDecoration| (|LocationBarViewMac|
+ // calls those).
+ NSPoint GetBubblePointInFrame(NSRect frame);
+
+ void SetToolTip(NSString* tooltip);
+
+ scoped_ptr<ContentSettingImageModel> content_setting_image_model_;
+
+ LocationBarViewMac* owner_; // weak
+ Profile* profile_; // weak
+
+ scoped_nsobject<NSString> tooltip_;
+
+ DISALLOW_COPY_AND_ASSIGN(ContentSettingDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_CONTENT_SETTING_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/content_setting_decoration.mm b/chrome/browser/ui/cocoa/location_bar/content_setting_decoration.mm
new file mode 100644
index 0000000..c803d13
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/content_setting_decoration.mm
@@ -0,0 +1,109 @@
+// 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/ui/cocoa/location_bar/content_setting_decoration.h"
+
+#include "app/resource_bundle.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/browser_list.h"
+#include "chrome/browser/content_setting_image_model.h"
+#include "chrome/browser/content_setting_bubble_model.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#import "chrome/browser/ui/cocoa/content_setting_bubble_cocoa.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
+#include "chrome/common/pref_names.h"
+#include "net/base/net_util.h"
+
+namespace {
+
+// How far to offset up from the bottom of the view to get the top
+// border of the popup 2px below the bottom of the Omnibox.
+const CGFloat kPopupPointYOffset = 2.0;
+
+} // namespace
+
+ContentSettingDecoration::ContentSettingDecoration(
+ ContentSettingsType settings_type,
+ LocationBarViewMac* owner,
+ Profile* profile)
+ : content_setting_image_model_(
+ ContentSettingImageModel::CreateContentSettingImageModel(
+ settings_type)),
+ owner_(owner),
+ profile_(profile) {
+}
+
+ContentSettingDecoration::~ContentSettingDecoration() {
+}
+
+bool ContentSettingDecoration::UpdateFromTabContents(
+ TabContents* tab_contents) {
+ bool was_visible = IsVisible();
+ int old_icon = content_setting_image_model_->get_icon();
+ content_setting_image_model_->UpdateFromTabContents(tab_contents);
+ SetVisible(content_setting_image_model_->is_visible());
+ bool decoration_changed = was_visible != IsVisible() ||
+ old_icon != content_setting_image_model_->get_icon();
+ if (IsVisible()) {
+ // TODO(thakis): We should use pdfs for these icons on OSX.
+ // http://crbug.com/35847
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ SetImage(rb.GetNativeImageNamed(content_setting_image_model_->get_icon()));
+ SetToolTip(base::SysUTF8ToNSString(
+ content_setting_image_model_->get_tooltip()));
+ }
+ return decoration_changed;
+}
+
+NSPoint ContentSettingDecoration::GetBubblePointInFrame(NSRect frame) {
+ const NSRect draw_frame = GetDrawRectInFrame(frame);
+ return NSMakePoint(NSMidX(draw_frame),
+ NSMaxY(draw_frame) - kPopupPointYOffset);
+}
+
+bool ContentSettingDecoration::OnMousePressed(NSRect frame) {
+ // Get host. This should be shared on linux/win/osx medium-term.
+ TabContents* tabContents =
+ BrowserList::GetLastActive()->GetSelectedTabContents();
+ if (!tabContents)
+ return true;
+
+ GURL url = tabContents->GetURL();
+ std::wstring displayHost;
+ net::AppendFormattedHost(
+ url,
+ UTF8ToWide(profile_->GetPrefs()->GetString(prefs::kAcceptLanguages)),
+ &displayHost, NULL, NULL);
+
+ // Find point for bubble's arrow in screen coordinates.
+ // TODO(shess): |owner_| is only being used to fetch |field|.
+ // Consider passing in |control_view|. Or refactoring to be
+ // consistent with other decorations (which don't currently bring up
+ // their bubble directly).
+ AutocompleteTextField* field = owner_->GetAutocompleteTextField();
+ NSPoint anchor = GetBubblePointInFrame(frame);
+ anchor = [field convertPoint:anchor toView:nil];
+ anchor = [[field window] convertBaseToScreen:anchor];
+
+ // Open bubble.
+ ContentSettingBubbleModel* model =
+ ContentSettingBubbleModel::CreateContentSettingBubbleModel(
+ tabContents, profile_,
+ content_setting_image_model_->get_content_settings_type());
+ [ContentSettingBubbleController showForModel:model
+ parentWindow:[field window]
+ anchoredAt:anchor];
+ return true;
+}
+
+NSString* ContentSettingDecoration::GetToolTip() {
+ return tooltip_.get();
+}
+
+void ContentSettingDecoration::SetToolTip(NSString* tooltip) {
+ tooltip_.reset([tooltip retain]);
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.h b/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.h
new file mode 100644
index 0000000..fb13de1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.h
@@ -0,0 +1,59 @@
+// 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_UI_COCOA_LOCATION_BAR_EV_BUBBLE_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_EV_BUBBLE_DECORATION_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/ui/cocoa/location_bar/bubble_decoration.h"
+
+// Draws the "Extended Validation SSL" bubble. This will be a lock
+// icon plus a label from the certification, and will replace the
+// location icon for URLs which have an EV cert. The |location_icon|
+// is used to fulfill drag-related calls.
+
+// TODO(shess): Refactor to pull the |location_icon| functionality out
+// into a distinct class like views |ClickHandler|.
+// http://crbug.com/48866
+
+class LocationIconDecoration;
+
+class EVBubbleDecoration : public BubbleDecoration {
+ public:
+ EVBubbleDecoration(LocationIconDecoration* location_icon, NSFont* font);
+
+ // |GetWidthForSpace()| will set |full_label| as the label, if it
+ // fits, else it will set an elided version.
+ void SetFullLabel(NSString* full_label);
+
+ // Get the point where the page info bubble should point within the
+ // decoration's frame, in the cell's coordinates.
+ NSPoint GetBubblePointInFrame(NSRect frame);
+
+ // Implement |LocationBarDecoration|.
+ virtual CGFloat GetWidthForSpace(CGFloat width);
+ virtual bool IsDraggable();
+ virtual NSPasteboard* GetDragPasteboard();
+ virtual NSImage* GetDragImage();
+ virtual NSRect GetDragImageFrame(NSRect frame) {
+ return GetImageRectInFrame(frame);
+ }
+ virtual bool OnMousePressed(NSRect frame);
+ virtual bool AcceptsMousePress() { return true; }
+
+ private:
+ // Keeps a reference to the font for use when eliding.
+ scoped_nsobject<NSFont> font_;
+
+ // The real label. BubbleDecoration's label may be elided.
+ scoped_nsobject<NSString> full_label_;
+
+ LocationIconDecoration* location_icon_; // weak, owned by location bar.
+
+ DISALLOW_COPY_AND_ASSIGN(EVBubbleDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_EV_BUBBLE_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.mm b/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.mm
new file mode 100644
index 0000000..ad384f9
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.mm
@@ -0,0 +1,117 @@
+// 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/ui/cocoa/location_bar/ev_bubble_decoration.h"
+
+#include "app/text_elider.h"
+#import "base/logging.h"
+#include "base/sys_string_conversions.h"
+#import "chrome/browser/ui/cocoa/image_utils.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h"
+#include "gfx/font.h"
+
+namespace {
+
+// TODO(shess): In general, decorations that don't fit in the
+// available space are omitted. This one never goes to omitted, it
+// sticks at 150px, which AFAICT follows the Windows code. Since the
+// Layout() code doesn't take this into account, it's possible the
+// control could end up with display artifacts, though things still
+// work (and don't crash).
+// http://crbug.com/49822
+
+// Minimum acceptable width for the ev bubble.
+const CGFloat kMinElidedBubbleWidth = 150.0;
+
+// Maximum amount of available space to make the bubble, subject to
+// |kMinElidedBubbleWidth|.
+const float kMaxBubbleFraction = 0.5;
+
+// The info-bubble point should look like it points to the bottom of the lock
+// icon. Determined with Pixie.app.
+const CGFloat kPageInfoBubblePointYOffset = 6.0;
+
+// TODO(shess): This is ugly, find a better way. Using it right now
+// so that I can crib from gtk and still be able to see that I'm using
+// the same values easily.
+NSColor* ColorWithRGBBytes(int rr, int gg, int bb) {
+ DCHECK_LE(rr, 255);
+ DCHECK_LE(bb, 255);
+ DCHECK_LE(gg, 255);
+ return [NSColor colorWithCalibratedRed:static_cast<float>(rr)/255.0
+ green:static_cast<float>(gg)/255.0
+ blue:static_cast<float>(bb)/255.0
+ alpha:1.0];
+}
+
+} // namespace
+
+EVBubbleDecoration::EVBubbleDecoration(
+ LocationIconDecoration* location_icon,
+ NSFont* font)
+ : BubbleDecoration(font),
+ font_([font retain]),
+ location_icon_(location_icon) {
+ // Color tuples stolen from location_bar_view_gtk.cc.
+ NSColor* border_color = ColorWithRGBBytes(0x90, 0xc3, 0x90);
+ NSColor* background_color = ColorWithRGBBytes(0xef, 0xfc, 0xef);
+ NSColor* text_color = ColorWithRGBBytes(0x07, 0x95, 0x00);
+ SetColors(border_color, background_color, text_color);
+}
+
+void EVBubbleDecoration::SetFullLabel(NSString* label) {
+ full_label_.reset([label retain]);
+ SetLabel(full_label_);
+}
+
+NSPoint EVBubbleDecoration::GetBubblePointInFrame(NSRect frame) {
+ NSRect image_rect = GetImageRectInFrame(frame);
+ return NSMakePoint(NSMidX(image_rect),
+ NSMaxY(image_rect) - kPageInfoBubblePointYOffset);
+}
+
+CGFloat EVBubbleDecoration::GetWidthForSpace(CGFloat width) {
+ // Limit with to not take up too much of the available width, but
+ // also don't let it shrink too much.
+ width = std::max(width * kMaxBubbleFraction, kMinElidedBubbleWidth);
+
+ // Use the full label if it fits.
+ NSImage* image = GetImage();
+ const CGFloat all_width = GetWidthForImageAndLabel(image, full_label_);
+ if (all_width <= width) {
+ SetLabel(full_label_);
+ return all_width;
+ }
+
+ // Width left for laying out the label.
+ const CGFloat width_left = width - GetWidthForImageAndLabel(image, @"");
+
+ // Middle-elide the label to fit |width_left|. This leaves the
+ // prefix and the trailing country code in place.
+ gfx::Font font(base::SysNSStringToWide([font_ fontName]),
+ [font_ pointSize]);
+ NSString* elided_label = base::SysUTF16ToNSString(
+ ElideText(base::SysNSStringToUTF16(full_label_), font, width_left, true));
+
+ // Use the elided label.
+ SetLabel(elided_label);
+ return GetWidthForImageAndLabel(image, elided_label);
+}
+
+// Pass mouse operations through to location icon.
+bool EVBubbleDecoration::IsDraggable() {
+ return location_icon_->IsDraggable();
+}
+
+NSPasteboard* EVBubbleDecoration::GetDragPasteboard() {
+ return location_icon_->GetDragPasteboard();
+}
+
+NSImage* EVBubbleDecoration::GetDragImage() {
+ return location_icon_->GetDragImage();
+}
+
+bool EVBubbleDecoration::OnMousePressed(NSRect frame) {
+ return location_icon_->OnMousePressed(frame);
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration_unittest.mm b/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration_unittest.mm
new file mode 100644
index 0000000..0506a72
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration_unittest.mm
@@ -0,0 +1,55 @@
+// 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/ui/cocoa/location_bar/ev_bubble_decoration.h"
+
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+class EVBubbleDecorationTest : public CocoaTest {
+ public:
+ EVBubbleDecorationTest()
+ : decoration_(NULL, [NSFont userFontOfSize:12]) {
+ }
+
+ EVBubbleDecoration decoration_;
+};
+
+// Test that the decoration gets smaller when there's not enough space
+// to fit, within bounds.
+TEST_F(EVBubbleDecorationTest, MiddleElide) {
+ NSString* kLongString = @"A very long string with spaces";
+ const CGFloat kWide = 1000.0; // Wide enough to fit everything.
+ const CGFloat kNarrow = 10.0; // Too narrow for anything.
+ const CGFloat kMinimumWidth = 100.0; // Never should get this small.
+
+ const NSSize kImageSize = NSMakeSize(20.0, 20.0);
+ scoped_nsobject<NSImage> image([[NSImage alloc] initWithSize:kImageSize]);
+
+ decoration_.SetImage(image);
+ decoration_.SetFullLabel(kLongString);
+
+ // Lots of space, decoration not omitted.
+ EXPECT_NE(decoration_.GetWidthForSpace(kWide),
+ LocationBarDecoration::kOmittedWidth);
+
+ // If the available space is of the same magnitude as the required
+ // space, the decoration doesn't eat it all up.
+ const CGFloat long_width = decoration_.GetWidthForSpace(kWide);
+ EXPECT_NE(decoration_.GetWidthForSpace(long_width + 20.0),
+ LocationBarDecoration::kOmittedWidth);
+ EXPECT_LT(decoration_.GetWidthForSpace(long_width + 20.0), long_width);
+
+ // If there is very little space, the decoration is still relatively
+ // big.
+ EXPECT_NE(decoration_.GetWidthForSpace(kNarrow),
+ LocationBarDecoration::kOmittedWidth);
+ EXPECT_GT(decoration_.GetWidthForSpace(kNarrow), kMinimumWidth);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/image_decoration.h b/chrome/browser/ui/cocoa/location_bar/image_decoration.h
new file mode 100644
index 0000000..c0bcfbf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/image_decoration.h
@@ -0,0 +1,36 @@
+// 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_UI_COCOA_LOCATION_BAR_IMAGE_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_IMAGE_DECORATION_H_
+#pragma once
+
+#import "base/scoped_nsobject.h"
+#include "chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h"
+
+// |LocationBarDecoration| which sizes and draws itself according to
+// an |NSImage|.
+
+class ImageDecoration : public LocationBarDecoration {
+ public:
+ ImageDecoration();
+ virtual ~ImageDecoration();
+
+ NSImage* GetImage();
+ void SetImage(NSImage* image);
+
+ // Returns the part of |frame| the image is drawn in.
+ NSRect GetDrawRectInFrame(NSRect frame);
+
+ // Implement |LocationBarDecoration|.
+ virtual CGFloat GetWidthForSpace(CGFloat width);
+ virtual void DrawInFrame(NSRect frame, NSView* control_view);
+
+ private:
+ scoped_nsobject<NSImage> image_;
+
+ DISALLOW_COPY_AND_ASSIGN(ImageDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_IMAGE_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/image_decoration.mm b/chrome/browser/ui/cocoa/location_bar/image_decoration.mm
new file mode 100644
index 0000000..8056a4e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/image_decoration.mm
@@ -0,0 +1,54 @@
+// 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 <cmath>
+
+#import "chrome/browser/ui/cocoa/location_bar/image_decoration.h"
+
+#import "chrome/browser/ui/cocoa/image_utils.h"
+
+ImageDecoration::ImageDecoration() {
+}
+
+ImageDecoration::~ImageDecoration() {
+}
+
+NSImage* ImageDecoration::GetImage() {
+ return image_;
+}
+
+void ImageDecoration::SetImage(NSImage* image) {
+ image_.reset([image retain]);
+}
+
+NSRect ImageDecoration::GetDrawRectInFrame(NSRect frame) {
+ NSImage* image = GetImage();
+ if (!image)
+ return frame;
+
+ // Center the image within the frame.
+ const CGFloat delta_height = NSHeight(frame) - [image size].height;
+ const CGFloat y_inset = std::floor(delta_height / 2.0);
+ const CGFloat delta_width = NSWidth(frame) - [image size].width;
+ const CGFloat x_inset = std::floor(delta_width / 2.0);
+ return NSInsetRect(frame, x_inset, y_inset);
+}
+
+CGFloat ImageDecoration::GetWidthForSpace(CGFloat width) {
+ NSImage* image = GetImage();
+ if (image) {
+ const CGFloat image_width = [image size].width;
+ if (image_width <= width)
+ return image_width;
+ }
+ return kOmittedWidth;
+}
+
+void ImageDecoration::DrawInFrame(NSRect frame, NSView* control_view) {
+ [GetImage() drawInRect:GetDrawRectInFrame(frame)
+ fromRect:NSZeroRect // Entire image
+ operation:NSCompositeSourceOver
+ fraction:1.0
+ neverFlipped:YES];
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/image_decoration_unittest.mm b/chrome/browser/ui/cocoa/location_bar/image_decoration_unittest.mm
new file mode 100644
index 0000000..db69b0d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/image_decoration_unittest.mm
@@ -0,0 +1,55 @@
+// 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/ui/cocoa/location_bar/image_decoration.h"
+
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+class ImageDecorationTest : public CocoaTest {
+ public:
+ ImageDecoration decoration_;
+};
+
+TEST_F(ImageDecorationTest, SetGetImage) {
+ EXPECT_FALSE(decoration_.GetImage());
+
+ const NSSize kImageSize = NSMakeSize(20.0, 20.0);
+ scoped_nsobject<NSImage> image([[NSImage alloc] initWithSize:kImageSize]);
+
+ decoration_.SetImage(image);
+ EXPECT_EQ(decoration_.GetImage(), image);
+
+ decoration_.SetImage(nil);
+ EXPECT_FALSE(decoration_.GetImage());
+}
+
+TEST_F(ImageDecorationTest, GetWidthForSpace) {
+ const CGFloat kWide = 100.0;
+ const CGFloat kNarrow = 10.0;
+
+ // Decoration with no image is omitted.
+ EXPECT_EQ(decoration_.GetWidthForSpace(kWide),
+ LocationBarDecoration::kOmittedWidth);
+
+ const NSSize kImageSize = NSMakeSize(20.0, 20.0);
+ scoped_nsobject<NSImage> image([[NSImage alloc] initWithSize:kImageSize]);
+
+ // Decoration takes up the space of the image.
+ decoration_.SetImage(image);
+ EXPECT_EQ(decoration_.GetWidthForSpace(kWide), kImageSize.width);
+
+ // If the image doesn't fit, decoration is omitted.
+ EXPECT_EQ(decoration_.GetWidthForSpace(kNarrow),
+ LocationBarDecoration::kOmittedWidth);
+}
+
+// TODO(shess): It would be nice to test mouse clicks and dragging,
+// but those are hard because they require a real |owner|.
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.h b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.h
new file mode 100644
index 0000000..97a6b3e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.h
@@ -0,0 +1,43 @@
+// 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_UI_COCOA_LOCATION_BAR_INSTANT_OPT_IN_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_INSTANT_OPT_IN_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+class Profile;
+
+// This delegate receives callbacks from the InstantOptInController when the OK
+// and Cancel buttons are pushed.
+class InstantOptInControllerDelegate {
+ public:
+ virtual void UserPressedOptIn(bool opt_in) = 0;
+
+ protected:
+ virtual ~InstantOptInControllerDelegate() {}
+};
+
+// Manages an instant opt-in view, which is part of the omnibox popup.
+@interface InstantOptInController : NSViewController {
+ @private
+ InstantOptInControllerDelegate* delegate_; // weak
+
+ // Needed in order to localize text and resize to fit.
+ IBOutlet NSButton* okButton_;
+ IBOutlet NSButton* cancelButton_;
+ IBOutlet NSTextField* label_;
+}
+
+// Designated initializer.
+- (id)initWithDelegate:(InstantOptInControllerDelegate*)delegate;
+
+// Button actions.
+- (IBAction)ok:(id)sender;
+- (IBAction)cancel:(id)sender;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_INSTANT_OPT_IN_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.mm b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.mm
new file mode 100644
index 0000000..17b0d15
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.mm
@@ -0,0 +1,31 @@
+// 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/ui/cocoa/location_bar/instant_opt_in_controller.h"
+
+#include "base/mac_util.h"
+
+@implementation InstantOptInController
+
+- (id)initWithDelegate:(InstantOptInControllerDelegate*)delegate {
+ if ((self = [super initWithNibName:@"InstantOptIn"
+ bundle:mac_util::MainAppBundle()])) {
+ delegate_ = delegate;
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ // TODO(rohitrao): Translate and resize strings.
+}
+
+- (IBAction)ok:(id)sender {
+ delegate_->UserPressedOptIn(true);
+}
+
+- (IBAction)cancel:(id)sender {
+ delegate_->UserPressedOptIn(false);
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller_unittest.mm b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller_unittest.mm
new file mode 100644
index 0000000..81ef513
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller_unittest.mm
@@ -0,0 +1,62 @@
+// 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"
+#include "base/scoped_ptr.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface InstantOptInController (ExposedForTesting)
+- (NSButton*)okButton;
+- (NSButton*)cancelButton;
+@end
+
+@implementation InstantOptInController (ExposedForTesting)
+- (NSButton*)okButton {
+ return okButton_;
+}
+
+- (NSButton*)cancelButton {
+ return cancelButton_;
+}
+@end
+
+
+namespace {
+
+class MockDelegate : public InstantOptInControllerDelegate {
+ public:
+ MOCK_METHOD1(UserPressedOptIn, void(bool opt_in));
+};
+
+class InstantOptInControllerTest : public CocoaTest {
+ public:
+ void SetUp() {
+ CocoaTest::SetUp();
+
+ controller_.reset(
+ [[InstantOptInController alloc] initWithDelegate:&delegate_]);
+
+ NSView* parent = [test_window() contentView];
+ [parent addSubview:[controller_ view]];
+ }
+
+ MockDelegate delegate_;
+ scoped_nsobject<InstantOptInController> controller_;
+};
+
+TEST_F(InstantOptInControllerTest, OkButtonCallback) {
+ EXPECT_CALL(delegate_, UserPressedOptIn(true));
+ [[controller_ okButton] performClick:nil];
+}
+
+TEST_F(InstantOptInControllerTest, CancelButtonCallback) {
+ EXPECT_CALL(delegate_, UserPressedOptIn(false));
+ [[controller_ cancelButton] performClick:nil];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.h b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.h
new file mode 100644
index 0000000..b9ef6bd
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.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_UI_COCOA_LOCATION_BAR_INSTANT_OPT_IN_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_INSTANT_OPT_IN_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// The instant opt in view that is embedded in the omnibox. Draws rounded
+// bottom corners and a horizontal gray line at the top.
+@interface InstantOptInView : NSView
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_INSTANT_OPT_IN_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.mm b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.mm
new file mode 100644
index 0000000..06ca79d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.mm
@@ -0,0 +1,54 @@
+// 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 <algorithm>
+
+#import "chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.h"
+#import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h"
+
+namespace {
+// How to round off the popup's corners. Goal is to match star and go
+// buttons.
+const CGFloat kPopupRoundingRadius = 3.5;
+
+// How far from the top of the view to place the horizontal line.
+const CGFloat kHorizontalLineTopOffset = 2;
+
+// How far from the sides to inset the horizontal line.
+const CGFloat kHorizontalLineInset = 2;
+}
+
+@implementation InstantOptInView
+
+- (void)drawRect:(NSRect)rect {
+ // Round off the bottom corners only.
+ NSBezierPath* path =
+ [NSBezierPath gtm_bezierPathWithRoundRect:[self bounds]
+ topLeftCornerRadius:0
+ topRightCornerRadius:0
+ bottomLeftCornerRadius:kPopupRoundingRadius
+ bottomRightCornerRadius:kPopupRoundingRadius];
+
+ [NSGraphicsContext saveGraphicsState];
+ [path addClip];
+
+ // Background is white.
+ [[NSColor whiteColor] set];
+ NSRectFill(rect);
+
+ // Draw a horizontal line 2 px down from the top of the view, inset at the
+ // sides by 2 px.
+ CGFloat lineY = NSMaxY([self bounds]) - kHorizontalLineTopOffset;
+ CGFloat minX = std::min(NSMinX([self bounds]) + kHorizontalLineInset,
+ NSMaxX([self bounds]));
+ CGFloat maxX = std::max(NSMaxX([self bounds]) - kHorizontalLineInset,
+ NSMinX([self bounds]));
+
+ [[NSColor lightGrayColor] set];
+ NSRectFill(NSMakeRect(minX, lineY, maxX - minX, 1));
+
+ [NSGraphicsContext restoreGraphicsState];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view_unittest.mm b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view_unittest.mm
new file mode 100644
index 0000000..ce5ff48
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/instant_opt_in_view_unittest.mm
@@ -0,0 +1,26 @@
+// 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/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.h"
+
+namespace {
+
+class InstantOptInViewTest : public CocoaTest {
+ public:
+ InstantOptInViewTest() {
+ NSRect content_frame = [[test_window() contentView] frame];
+ scoped_nsobject<InstantOptInView> view(
+ [[InstantOptInView alloc] initWithFrame:content_frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ InstantOptInView* view_; // Weak. Owned by the view hierarchy.
+};
+
+// Tests display, add/remove.
+TEST_VIEW(InstantOptInViewTest, view_);
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.h b/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.h
new file mode 100644
index 0000000..3b8c607
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.h
@@ -0,0 +1,47 @@
+// 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_UI_COCOA_LOCATION_BAR_KEYWORD_HINT_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_KEYWORD_HINT_DECORATION_H_
+#pragma once
+
+#include <string>
+
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h"
+
+#import "base/scoped_nsobject.h"
+
+// Draws the keyword hint, "Press [tab] to search <site>".
+
+class KeywordHintDecoration : public LocationBarDecoration {
+ public:
+ KeywordHintDecoration(NSFont* font);
+ virtual ~KeywordHintDecoration();
+
+ // Calculates the message to display and where to place the [tab]
+ // image.
+ void SetKeyword(const std::wstring& keyword, bool is_extension_keyword);
+
+ // Implement |LocationBarDecoration|.
+ virtual void DrawInFrame(NSRect frame, NSView* control_view);
+ virtual CGFloat GetWidthForSpace(CGFloat width);
+
+ private:
+ // Fetch and cache the [tab] image.
+ NSImage* GetHintImage();
+
+ // Attributes for drawing the hint string, such as font and color.
+ scoped_nsobject<NSDictionary> attributes_;
+
+ // Cache for the [tab] image.
+ scoped_nsobject<NSImage> hint_image_;
+
+ // The text to display to the left and right of the hint image.
+ scoped_nsobject<NSString> hint_prefix_;
+ scoped_nsobject<NSString> hint_suffix_;
+
+ DISALLOW_COPY_AND_ASSIGN(KeywordHintDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_KEYWORD_HINT_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.mm b/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.mm
new file mode 100644
index 0000000..0b080319
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.mm
@@ -0,0 +1,160 @@
+// 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 <cmath>
+
+#import "chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.h"
+
+#include "app/l10n_util.h"
+#include "app/resource_bundle.h"
+#include "base/logging.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#import "chrome/browser/ui/cocoa/image_utils.h"
+#include "grit/theme_resources.h"
+#include "grit/generated_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+
+namespace {
+
+// How far to inset the hint text area from sides.
+const CGFloat kHintTextYInset = 4.0;
+
+// How far to inset the hint image from sides. Lines baseline of text
+// in image with baseline of prefix and suffix.
+const CGFloat kHintImageYInset = 4.0;
+
+// Extra padding right and left of the image.
+const CGFloat kHintImagePadding = 1.0;
+
+// Maxmimum of the available space to allow the hint to take over.
+// Should leave enough so that the user has space to edit things.
+const CGFloat kHintAvailableRatio = 2.0 / 3.0;
+
+// Helper to convert |s| to an |NSString|, trimming whitespace at
+// ends.
+NSString* TrimAndConvert(const std::wstring& s) {
+ std::wstring output;
+ TrimWhitespace(s, TRIM_ALL, &output);
+ return base::SysWideToNSString(output);
+}
+
+} // namespace
+
+KeywordHintDecoration::KeywordHintDecoration(NSFont* font) {
+ NSColor* text_color = [NSColor lightGrayColor];
+ NSDictionary* attributes =
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ font, NSFontAttributeName,
+ text_color, NSForegroundColorAttributeName,
+ nil];
+ attributes_.reset([attributes retain]);
+}
+
+KeywordHintDecoration::~KeywordHintDecoration() {
+}
+
+NSImage* KeywordHintDecoration::GetHintImage() {
+ if (!hint_image_) {
+ SkBitmap* skiaBitmap = ResourceBundle::GetSharedInstance().
+ GetBitmapNamed(IDR_LOCATION_BAR_KEYWORD_HINT_TAB);
+ if (skiaBitmap)
+ hint_image_.reset([gfx::SkBitmapToNSImage(*skiaBitmap) retain]);
+ }
+ return hint_image_;
+}
+
+void KeywordHintDecoration::SetKeyword(const std::wstring& short_name,
+ bool is_extension_keyword) {
+ // KEYWORD_HINT is a message like "Press [tab] to search <site>".
+ // [tab] is a parameter to be replaced by an image. "<site>" is
+ // derived from |short_name|.
+ std::vector<size_t> content_param_offsets;
+ int message_id = is_extension_keyword ?
+ IDS_OMNIBOX_EXTENSION_KEYWORD_HINT : IDS_OMNIBOX_KEYWORD_HINT;
+ const std::wstring keyword_hint(
+ l10n_util::GetStringF(message_id,
+ std::wstring(), short_name,
+ &content_param_offsets));
+
+ // Should always be 2 offsets, see the comment in
+ // location_bar_view.cc after IDS_OMNIBOX_KEYWORD_HINT fetch.
+ DCHECK_EQ(content_param_offsets.size(), 2U);
+
+ // Where to put the [tab] image.
+ const size_t split = content_param_offsets.front();
+
+ // Trim the spaces from the edges (there is space in the image) and
+ // convert to |NSString|.
+ hint_prefix_.reset([TrimAndConvert(keyword_hint.substr(0, split)) retain]);
+ hint_suffix_.reset([TrimAndConvert(keyword_hint.substr(split)) retain]);
+}
+
+CGFloat KeywordHintDecoration::GetWidthForSpace(CGFloat width) {
+ NSImage* image = GetHintImage();
+ const CGFloat image_width = image ? [image size].width : 0.0;
+
+ // AFAICT, on Windows the choices are "everything" if it fits, then
+ // "image only" if it fits.
+
+ // Entirely too small to fit, omit.
+ if (width < image_width)
+ return kOmittedWidth;
+
+ // Show the full hint if it won't take up too much space. The image
+ // needs to be placed at a pixel boundary, round the text widths so
+ // that any partially-drawn pixels don't look too close (or too
+ // far).
+ CGFloat full_width =
+ std::floor([hint_prefix_ sizeWithAttributes:attributes_].width + 0.5) +
+ kHintImagePadding + image_width + kHintImagePadding +
+ std::floor([hint_suffix_ sizeWithAttributes:attributes_].width + 0.5);
+ if (full_width <= width * kHintAvailableRatio)
+ return full_width;
+
+ return image_width;
+}
+
+void KeywordHintDecoration::DrawInFrame(NSRect frame, NSView* control_view) {
+ NSImage* image = GetHintImage();
+ const CGFloat image_width = image ? [image size].width : 0.0;
+
+ const bool draw_full = NSWidth(frame) > image_width;
+
+ if (draw_full) {
+ NSRect prefix_rect = NSInsetRect(frame, 0.0, kHintTextYInset);
+ const CGFloat prefix_width =
+ [hint_prefix_ sizeWithAttributes:attributes_].width;
+ DCHECK_GE(NSWidth(prefix_rect), prefix_width);
+ [hint_prefix_ drawInRect:prefix_rect withAttributes:attributes_];
+
+ // The image should be drawn at a pixel boundary, round the prefix
+ // so that partial pixels aren't oddly close (or distant).
+ frame.origin.x += std::floor(prefix_width + 0.5) + kHintImagePadding;
+ frame.size.width -= std::floor(prefix_width + 0.5) + kHintImagePadding;
+ }
+
+ NSRect image_rect = NSInsetRect(frame, 0.0, kHintImageYInset);
+ image_rect.size = [image size];
+ [image drawInRect:image_rect
+ fromRect:NSZeroRect // Entire image
+ operation:NSCompositeSourceOver
+ fraction:1.0
+ neverFlipped:YES];
+ frame.origin.x += NSWidth(image_rect);
+ frame.size.width -= NSWidth(image_rect);
+
+ if (draw_full) {
+ NSRect suffix_rect = NSInsetRect(frame, 0.0, kHintTextYInset);
+ const CGFloat suffix_width =
+ [hint_suffix_ sizeWithAttributes:attributes_].width;
+
+ // Right-justify the text within the remaining space, so it
+ // doesn't get too close to the image relative to a following
+ // decoration.
+ suffix_rect.origin.x = NSMaxX(suffix_rect) - suffix_width;
+ DCHECK_GE(NSWidth(suffix_rect), suffix_width);
+ [hint_suffix_ drawInRect:suffix_rect withAttributes:attributes_];
+ }
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration_unittest.mm b/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration_unittest.mm
new file mode 100644
index 0000000..bfcf454
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration_unittest.mm
@@ -0,0 +1,57 @@
+// 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/ui/cocoa/location_bar/keyword_hint_decoration.h"
+
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+class KeywordHintDecorationTest : public CocoaTest {
+ public:
+ KeywordHintDecorationTest()
+ : decoration_(NULL) {
+ }
+
+ KeywordHintDecoration decoration_;
+};
+
+TEST_F(KeywordHintDecorationTest, GetWidthForSpace) {
+ decoration_.SetVisible(true);
+ decoration_.SetKeyword(std::wstring(L"Google"), false);
+
+ const CGFloat kVeryWide = 1000.0;
+ const CGFloat kFairlyWide = 100.0; // Estimate for full hint space.
+ const CGFloat kEditingSpace = 50.0;
+
+ // Wider than the [tab] image when we have lots of space.
+ EXPECT_NE(decoration_.GetWidthForSpace(kVeryWide),
+ LocationBarDecoration::kOmittedWidth);
+ EXPECT_GE(decoration_.GetWidthForSpace(kVeryWide), kFairlyWide);
+
+ // When there's not enough space for the text, trims to something
+ // narrower.
+ const CGFloat full_width = decoration_.GetWidthForSpace(kVeryWide);
+ const CGFloat not_wide_enough = full_width - 10.0;
+ EXPECT_NE(decoration_.GetWidthForSpace(not_wide_enough),
+ LocationBarDecoration::kOmittedWidth);
+ EXPECT_LT(decoration_.GetWidthForSpace(not_wide_enough), full_width);
+
+ // Even trims when there's enough space for everything, but it would
+ // eat "too much".
+ EXPECT_NE(decoration_.GetWidthForSpace(full_width + kEditingSpace),
+ LocationBarDecoration::kOmittedWidth);
+ EXPECT_LT(decoration_.GetWidthForSpace(full_width + kEditingSpace),
+ full_width);
+
+ // Omitted when not wide enough to fit even the image.
+ const CGFloat image_width = decoration_.GetWidthForSpace(not_wide_enough);
+ EXPECT_EQ(decoration_.GetWidthForSpace(image_width - 1.0),
+ LocationBarDecoration::kOmittedWidth);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h b/chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h
new file mode 100644
index 0000000..5947edc
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/location_bar_decoration.h
@@ -0,0 +1,89 @@
+// 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_UI_COCOA_LOCATION_BAR_LOCATION_BAR_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_LOCATION_BAR_DECORATION_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/basictypes.h"
+
+// Base class for decorations at the left and right of the location
+// bar. For instance, the location icon.
+
+// |LocationBarDecoration| and subclasses should approximately
+// parallel the classes provided under views/location_bar/. The term
+// "decoration" is used because "view" has strong connotations in
+// Cocoa, and while these are view-like, they aren't views at all.
+// Decorations are more like Cocoa cells, except implemented in C++ to
+// allow more similarity to the other platform implementations.
+
+class LocationBarDecoration {
+ public:
+ LocationBarDecoration()
+ : visible_(false) {
+ }
+ virtual ~LocationBarDecoration() {}
+
+ // Determines whether the decoration is visible.
+ virtual bool IsVisible() const {
+ return visible_;
+ }
+ virtual void SetVisible(bool visible) {
+ visible_ = visible;
+ }
+
+ // Decorations can change their size to fit the available space.
+ // Returns the width the decoration will use in the space allotted,
+ // or |kOmittedWidth| if it should be omitted.
+ virtual CGFloat GetWidthForSpace(CGFloat width);
+
+ // Draw the decoration in the frame provided. The frame will be
+ // generated from an earlier call to |GetWidthForSpace()|.
+ virtual void DrawInFrame(NSRect frame, NSView* control_view);
+
+ // Returns the tooltip for this decoration, return |nil| for no tooltip.
+ virtual NSString* GetToolTip() { return nil; }
+
+ // Decorations which do not accept mouse events are treated like the
+ // field's background for purposes of selecting text. When such
+ // decorations are adjacent to the text area, they will show the
+ // I-beam cursor. Decorations which do accept mouse events will get
+ // an arrow cursor when the mouse is over them.
+ virtual bool AcceptsMousePress() { return false; }
+
+ // Determine if the item can act as a drag source.
+ virtual bool IsDraggable() { return false; }
+
+ // The image to drag.
+ virtual NSImage* GetDragImage() { return nil; }
+
+ // Return the place within the decoration's frame where the
+ // |GetDragImage()| comes from. This is used to make sure the image
+ // appears correctly under the mouse while dragging. |frame|
+ // matches the frame passed to |DrawInFrame()|.
+ virtual NSRect GetDragImageFrame(NSRect frame) { return NSZeroRect; }
+
+ // The pasteboard to drag.
+ virtual NSPasteboard* GetDragPasteboard() { return nil; }
+
+ // Called on mouse down. Return |false| to indicate that the press
+ // was not processed and should be handled by the cell.
+ virtual bool OnMousePressed(NSRect frame) { return false; }
+
+ // Called to get the right-click menu, return |nil| for no menu.
+ virtual NSMenu* GetMenu() { return nil; }
+
+ // Width returned by |GetWidthForSpace()| when the item should be
+ // omitted for this width;
+ static const CGFloat kOmittedWidth;
+
+ private:
+ bool visible_;
+
+ DISALLOW_COPY_AND_ASSIGN(LocationBarDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_LOCATION_BAR_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/location_bar_decoration.mm b/chrome/browser/ui/cocoa/location_bar/location_bar_decoration.mm
new file mode 100644
index 0000000..bbd9b0b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/location_bar_decoration.mm
@@ -0,0 +1,18 @@
+// 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/ui/cocoa/location_bar/location_bar_decoration.h"
+
+#include "base/logging.h"
+
+const CGFloat LocationBarDecoration::kOmittedWidth = 0.0;
+
+CGFloat LocationBarDecoration::GetWidthForSpace(CGFloat width) {
+ NOTREACHED();
+ return kOmittedWidth;
+}
+
+void LocationBarDecoration::DrawInFrame(NSRect frame, NSView* control_view) {
+ NOTREACHED();
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h b/chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h
new file mode 100644
index 0000000..7675165d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h
@@ -0,0 +1,237 @@
+// 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_UI_COCOA_LOCATION_BAR_VIEW_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_VIEW_MAC_H_
+#pragma once
+
+#include <string>
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "base/scoped_vector.h"
+#include "chrome/browser/autocomplete/autocomplete_edit.h"
+#include "chrome/browser/autocomplete/autocomplete_edit_view_mac.h"
+#include "chrome/browser/extensions/image_loading_tracker.h"
+#include "chrome/browser/first_run/first_run.h"
+#include "chrome/browser/location_bar.h"
+#include "chrome/browser/toolbar_model.h"
+#include "chrome/common/content_settings_types.h"
+
+@class AutocompleteTextField;
+class CommandUpdater;
+class ContentSettingDecoration;
+class ContentSettingImageModel;
+class EVBubbleDecoration;
+@class ExtensionPopupController;
+class KeywordHintDecoration;
+class LocationIconDecoration;
+class PageActionDecoration;
+class Profile;
+class SelectedKeywordDecoration;
+class SkBitmap;
+class StarDecoration;
+class ToolbarModel;
+
+// A C++ bridge class that represents the location bar UI element to
+// the portable code. Wires up an AutocompleteEditViewMac instance to
+// the location bar text field, which handles most of the work.
+
+class LocationBarViewMac : public AutocompleteEditController,
+ public LocationBar,
+ public LocationBarTesting,
+ public NotificationObserver {
+ public:
+ LocationBarViewMac(AutocompleteTextField* field,
+ CommandUpdater* command_updater,
+ ToolbarModel* toolbar_model,
+ Profile* profile,
+ Browser* browser);
+ virtual ~LocationBarViewMac();
+
+ // Overridden from LocationBar:
+ virtual void ShowFirstRunBubble(FirstRun::BubbleType bubble_type);
+ virtual void SetSuggestedText(const string16& text);
+ virtual std::wstring GetInputString() const;
+ virtual WindowOpenDisposition GetWindowOpenDisposition() const;
+ virtual PageTransition::Type GetPageTransition() const;
+ virtual void AcceptInput();
+ virtual void FocusLocation(bool select_all);
+ virtual void FocusSearch();
+ virtual void UpdateContentSettingsIcons();
+ virtual void UpdatePageActions();
+ virtual void InvalidatePageActions();
+ virtual void SaveStateToContents(TabContents* contents);
+ virtual void Revert();
+ virtual const AutocompleteEditView* location_entry() const {
+ return edit_view_.get();
+ }
+ virtual AutocompleteEditView* location_entry() {
+ return edit_view_.get();
+ }
+ virtual LocationBarTesting* GetLocationBarForTesting() { return this; }
+
+ // Overridden from LocationBarTesting:
+ virtual int PageActionCount();
+ virtual int PageActionVisibleCount();
+ virtual ExtensionAction* GetPageAction(size_t index);
+ virtual ExtensionAction* GetVisiblePageAction(size_t index);
+ virtual void TestPageActionPressed(size_t index);
+
+ // Set/Get the editable state of the field.
+ void SetEditable(bool editable);
+ bool IsEditable();
+
+ // Set the starred state of the bookmark star.
+ void SetStarred(bool starred);
+
+ // Get the point on the star for the bookmark bubble to aim at.
+ NSPoint GetBookmarkBubblePoint() const;
+
+ // Get the point in the security icon at which the page info bubble aims.
+ NSPoint GetPageInfoBubblePoint() const;
+
+ // Get the point in the omnibox at which the first run bubble aims.
+ NSPoint GetFirstRunBubblePoint() const;
+
+ // Updates the location bar. Resets the bar's permanent text and
+ // security style, and if |should_restore_state| is true, restores
+ // saved state from the tab (for tab switching).
+ void Update(const TabContents* tab, bool should_restore_state);
+
+ // Layout the various decorations which live in the field.
+ void Layout();
+
+ // Returns the current TabContents.
+ TabContents* GetTabContents() const;
+
+ // Sets preview_enabled_ for the PageActionImageView associated with this
+ // |page_action|. If |preview_enabled|, the location bar will display the
+ // PageAction icon even if it has not been activated by the extension.
+ // This is used by the ExtensionInstalledBubble to preview what the icon
+ // will look like for the user upon installation of the extension.
+ void SetPreviewEnabledPageAction(ExtensionAction* page_action,
+ bool preview_enabled);
+
+ // Return |page_action|'s info-bubble point in window coordinates.
+ // This function should always be called with a visible page action.
+ // If |page_action| is not a page action or not visible, NOTREACHED()
+ // is called and this function returns |NSZeroPoint|.
+ NSPoint GetPageActionBubblePoint(ExtensionAction* page_action);
+
+ // Get the blocked-popup content setting's frame in window
+ // coordinates. Used by the blocked-popup animation. Returns
+ // |NSZeroRect| if the relevant content setting decoration is not
+ // visible.
+ NSRect GetBlockedPopupRect() const;
+
+ // AutocompleteEditController implementation.
+ virtual void OnAutocompleteWillClosePopup();
+ virtual void OnAutocompleteLosingFocus(gfx::NativeView unused);
+ virtual void OnAutocompleteWillAccept();
+ virtual bool OnCommitSuggestedText(const std::wstring& typed_text);
+ virtual void OnSetSuggestedSearchText(const string16& suggested_text);
+ virtual void OnPopupBoundsChanged(const gfx::Rect& bounds);
+ virtual void OnAutocompleteAccept(const GURL& url,
+ WindowOpenDisposition disposition,
+ PageTransition::Type transition,
+ const GURL& alternate_nav_url);
+ virtual void OnChanged();
+ virtual void OnSelectionBoundsChanged();
+ virtual void OnInputInProgress(bool in_progress);
+ virtual void OnKillFocus();
+ virtual void OnSetFocus();
+ virtual SkBitmap GetFavIcon() const;
+ virtual std::wstring GetTitle() const;
+
+ NSImage* GetKeywordImage(const std::wstring& keyword);
+
+ AutocompleteTextField* GetAutocompleteTextField() { return field_; }
+
+
+ // Overridden from NotificationObserver.
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+
+ private:
+ // Posts |notification| to the default notification center.
+ void PostNotification(NSString* notification);
+
+ // Return the decoration for |page_action|.
+ PageActionDecoration* GetPageActionDecoration(ExtensionAction* page_action);
+
+ // Clear the page-action decorations.
+ void DeletePageActionDecorations();
+
+ // Re-generate the page-action decorations from the profile's
+ // extension service.
+ void RefreshPageActionDecorations();
+
+ // Updates visibility of the content settings icons based on the current
+ // tab contents state.
+ bool RefreshContentSettingsDecorations();
+
+ void ShowFirstRunBubbleInternal(FirstRun::BubbleType bubble_type);
+
+ scoped_ptr<AutocompleteEditViewMac> edit_view_;
+
+ CommandUpdater* command_updater_; // Weak, owned by Browser.
+
+ AutocompleteTextField* field_; // owned by tab controller
+
+ // When we get an OnAutocompleteAccept notification from the autocomplete
+ // edit, we save the input string so we can give it back to the browser on
+ // the LocationBar interface via GetInputString().
+ std::wstring location_input_;
+
+ // The user's desired disposition for how their input should be opened.
+ WindowOpenDisposition disposition_;
+
+ // A decoration that shows an icon to the left of the address.
+ scoped_ptr<LocationIconDecoration> location_icon_decoration_;
+
+ // A decoration that shows the keyword-search bubble on the left.
+ scoped_ptr<SelectedKeywordDecoration> selected_keyword_decoration_;
+
+ // A decoration that shows a lock icon and ev-cert label in a bubble
+ // on the left.
+ scoped_ptr<EVBubbleDecoration> ev_bubble_decoration_;
+
+ // Bookmark star right of page actions.
+ scoped_ptr<StarDecoration> star_decoration_;
+
+ // Any installed Page Actions.
+ ScopedVector<PageActionDecoration> page_action_decorations_;
+
+ // The content blocked decorations.
+ ScopedVector<ContentSettingDecoration> content_setting_decorations_;
+
+ // Keyword hint decoration displayed on the right-hand side.
+ scoped_ptr<KeywordHintDecoration> keyword_hint_decoration_;
+
+ Profile* profile_;
+
+ Browser* browser_;
+
+ ToolbarModel* toolbar_model_; // Weak, owned by Browser.
+
+ // Whether or not to update the instant preview.
+ bool update_instant_;
+
+ // The transition type to use for the navigation.
+ PageTransition::Type transition_;
+
+ // Used to register for notifications received by NotificationObserver.
+ NotificationRegistrar registrar_;
+
+ // Used to schedule a task for the first run info bubble.
+ ScopedRunnableMethodFactory<LocationBarViewMac> first_run_bubble_;
+
+ DISALLOW_COPY_AND_ASSIGN(LocationBarViewMac);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_VIEW_MAC_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.mm b/chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.mm
new file mode 100644
index 0000000..6fbac24
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.mm
@@ -0,0 +1,690 @@
+// 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/ui/cocoa/location_bar/location_bar_view_mac.h"
+
+#include "app/l10n_util_mac.h"
+#include "app/resource_bundle.h"
+#include "base/nsimage_cache_mac.h"
+#include "base/stl_util-inl.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/alternate_nav_url_fetcher.h"
+#import "chrome/browser/app_controller_mac.h"
+#import "chrome/browser/autocomplete/autocomplete_edit_view_mac.h"
+#import "chrome/browser/autocomplete/autocomplete_popup_model.h"
+#include "chrome/browser/browser_list.h"
+#include "chrome/browser/command_updater.h"
+#include "chrome/browser/content_setting_image_model.h"
+#include "chrome/browser/content_setting_bubble_model.h"
+#include "chrome/browser/defaults.h"
+#include "chrome/browser/extensions/extension_browser_event_router.h"
+#include "chrome/browser/extensions/extensions_service.h"
+#include "chrome/browser/extensions/extension_tabs_module.h"
+#include "chrome/browser/instant/instant_controller.h"
+#include "chrome/browser/location_bar_util.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/search_engines/template_url.h"
+#include "chrome/browser/search_engines/template_url_model.h"
+#include "chrome/browser/tab_contents/navigation_entry.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#import "chrome/browser/ui/cocoa/content_setting_bubble_cocoa.h"
+#include "chrome/browser/ui/cocoa/event_utils.h"
+#import "chrome/browser/ui/cocoa/extensions/extension_action_context_menu.h"
+#import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
+#import "chrome/browser/ui/cocoa/first_run_bubble_controller.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_cell.h"
+#import "chrome/browser/ui/cocoa/location_bar/content_setting_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/ev_bubble_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/keyword_hint_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/page_action_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.h"
+#import "chrome/browser/ui/cocoa/location_bar/star_decoration.h"
+#include "chrome/common/extensions/extension.h"
+#include "chrome/common/extensions/extension_action.h"
+#include "chrome/common/extensions/extension_resource.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/pref_names.h"
+#include "net/base/net_util.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+namespace {
+
+// Vertical space between the bottom edge of the location_bar and the first run
+// bubble arrow point.
+const static int kFirstRunBubbleYOffset = 1;
+
+}
+
+// TODO(shess): This code is mostly copied from the gtk
+// implementation. Make sure it's all appropriate and flesh it out.
+
+LocationBarViewMac::LocationBarViewMac(
+ AutocompleteTextField* field,
+ CommandUpdater* command_updater,
+ ToolbarModel* toolbar_model,
+ Profile* profile,
+ Browser* browser)
+ : edit_view_(new AutocompleteEditViewMac(this, toolbar_model, profile,
+ command_updater, field)),
+ command_updater_(command_updater),
+ field_(field),
+ disposition_(CURRENT_TAB),
+ location_icon_decoration_(new LocationIconDecoration(this)),
+ selected_keyword_decoration_(
+ new SelectedKeywordDecoration(
+ AutocompleteEditViewMac::GetFieldFont())),
+ ev_bubble_decoration_(
+ new EVBubbleDecoration(location_icon_decoration_.get(),
+ AutocompleteEditViewMac::GetFieldFont())),
+ star_decoration_(new StarDecoration(command_updater)),
+ keyword_hint_decoration_(
+ new KeywordHintDecoration(AutocompleteEditViewMac::GetFieldFont())),
+ profile_(profile),
+ browser_(browser),
+ toolbar_model_(toolbar_model),
+ update_instant_(true),
+ transition_(PageTransition::TYPED),
+ first_run_bubble_(this) {
+ for (size_t i = 0; i < CONTENT_SETTINGS_NUM_TYPES; ++i) {
+ DCHECK_EQ(i, content_setting_decorations_.size());
+ ContentSettingsType type = static_cast<ContentSettingsType>(i);
+ content_setting_decorations_.push_back(
+ new ContentSettingDecoration(type, this, profile_));
+ }
+
+ registrar_.Add(this,
+ NotificationType::EXTENSION_PAGE_ACTION_VISIBILITY_CHANGED,
+ NotificationService::AllSources());
+}
+
+LocationBarViewMac::~LocationBarViewMac() {
+ // Disconnect from cell in case it outlives us.
+ [[field_ cell] clearDecorations];
+}
+
+void LocationBarViewMac::ShowFirstRunBubble(FirstRun::BubbleType bubble_type) {
+ // We need the browser window to be shown before we can show the bubble, but
+ // we get called before that's happened.
+ Task* task = first_run_bubble_.NewRunnableMethod(
+ &LocationBarViewMac::ShowFirstRunBubbleInternal, bubble_type);
+ MessageLoop::current()->PostTask(FROM_HERE, task);
+}
+
+void LocationBarViewMac::ShowFirstRunBubbleInternal(
+ FirstRun::BubbleType bubble_type) {
+ if (!field_ || ![field_ window])
+ return;
+
+ // The first run bubble's left edge should line up with the left edge of the
+ // omnibox. This is different from other bubbles, which line up at a point
+ // set by their top arrow. Because the BaseBubbleController adjusts the
+ // window origin left to account for the arrow spacing, the first run bubble
+ // moves the window origin right by this spacing, so that the
+ // BaseBubbleController will move it back to the correct position.
+ const NSPoint kOffset = NSMakePoint(
+ info_bubble::kBubbleArrowXOffset + info_bubble::kBubbleArrowWidth/2.0,
+ kFirstRunBubbleYOffset);
+ [FirstRunBubbleController showForView:field_ offset:kOffset profile:profile_];
+}
+
+std::wstring LocationBarViewMac::GetInputString() const {
+ return location_input_;
+}
+
+void LocationBarViewMac::SetSuggestedText(const string16& text) {
+ edit_view_->SetSuggestText(
+ edit_view_->model()->UseVerbatimInstant() ? string16() : text);
+}
+
+WindowOpenDisposition LocationBarViewMac::GetWindowOpenDisposition() const {
+ return disposition_;
+}
+
+PageTransition::Type LocationBarViewMac::GetPageTransition() const {
+ return transition_;
+}
+
+void LocationBarViewMac::AcceptInput() {
+ WindowOpenDisposition disposition =
+ event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
+ edit_view_->model()->AcceptInput(disposition, false);
+}
+
+void LocationBarViewMac::FocusLocation(bool select_all) {
+ edit_view_->FocusLocation(select_all);
+}
+
+void LocationBarViewMac::FocusSearch() {
+ edit_view_->SetForcedQuery();
+}
+
+void LocationBarViewMac::UpdateContentSettingsIcons() {
+ if (RefreshContentSettingsDecorations()) {
+ [field_ updateCursorAndToolTipRects];
+ [field_ setNeedsDisplay:YES];
+ }
+}
+
+void LocationBarViewMac::UpdatePageActions() {
+ size_t count_before = page_action_decorations_.size();
+ RefreshPageActionDecorations();
+ Layout();
+ if (page_action_decorations_.size() != count_before) {
+ NotificationService::current()->Notify(
+ NotificationType::EXTENSION_PAGE_ACTION_COUNT_CHANGED,
+ Source<LocationBar>(this),
+ NotificationService::NoDetails());
+ }
+}
+
+void LocationBarViewMac::InvalidatePageActions() {
+ size_t count_before = page_action_decorations_.size();
+ DeletePageActionDecorations();
+ Layout();
+ if (page_action_decorations_.size() != count_before) {
+ NotificationService::current()->Notify(
+ NotificationType::EXTENSION_PAGE_ACTION_COUNT_CHANGED,
+ Source<LocationBar>(this),
+ NotificationService::NoDetails());
+ }
+}
+
+void LocationBarViewMac::SaveStateToContents(TabContents* contents) {
+ // TODO(shess): Why SaveStateToContents vs SaveStateToTab?
+ edit_view_->SaveStateToTab(contents);
+}
+
+void LocationBarViewMac::Update(const TabContents* contents,
+ bool should_restore_state) {
+ bool star_enabled = browser_defaults::bookmarks_enabled &&
+ [field_ isEditable] && !toolbar_model_->input_in_progress();
+ command_updater_->UpdateCommandEnabled(IDC_BOOKMARK_PAGE, star_enabled);
+ star_decoration_->SetVisible(star_enabled);
+ RefreshPageActionDecorations();
+ RefreshContentSettingsDecorations();
+ // AutocompleteEditView restores state if the tab is non-NULL.
+ edit_view_->Update(should_restore_state ? contents : NULL);
+ OnChanged();
+}
+
+void LocationBarViewMac::OnAutocompleteWillClosePopup() {
+ if (!update_instant_)
+ return;
+
+ InstantController* controller = browser_->instant();
+ if (controller && !controller->commit_on_mouse_up())
+ controller->DestroyPreviewContents();
+ SetSuggestedText(string16());
+}
+
+void LocationBarViewMac::OnAutocompleteLosingFocus(gfx::NativeView unused) {
+ SetSuggestedText(string16());
+
+ InstantController* instant = browser_->instant();
+ if (!instant)
+ return;
+
+ if (!instant->is_active() || !instant->GetPreviewContents())
+ return;
+
+ // If |IsMouseDownFromActivate()| returns false, the RenderWidgetHostView did
+ // not receive a mouseDown event. Therefore, we should destroy the preview.
+ // Otherwise, the RWHV was clicked, so we commit the preview.
+ if (!instant->IsMouseDownFromActivate())
+ instant->DestroyPreviewContents();
+ else if (instant->IsShowingInstant())
+ instant->SetCommitOnMouseUp();
+ else
+ instant->CommitCurrentPreview(INSTANT_COMMIT_FOCUS_LOST);
+}
+
+void LocationBarViewMac::OnAutocompleteWillAccept() {
+ update_instant_ = false;
+}
+
+bool LocationBarViewMac::OnCommitSuggestedText(const std::wstring& typed_text) {
+ return edit_view_->CommitSuggestText();
+}
+
+void LocationBarViewMac::OnSetSuggestedSearchText(
+ const string16& suggested_text) {
+ SetSuggestedText(suggested_text);
+}
+
+void LocationBarViewMac::OnPopupBoundsChanged(const gfx::Rect& bounds) {
+ InstantController* instant = browser_->instant();
+ if (instant)
+ instant->SetOmniboxBounds(bounds);
+}
+
+void LocationBarViewMac::OnAutocompleteAccept(const GURL& url,
+ WindowOpenDisposition disposition,
+ PageTransition::Type transition,
+ const GURL& alternate_nav_url) {
+ // WARNING: don't add an early return here. The calls after the if must
+ // happen.
+ if (url.is_valid()) {
+ location_input_ = UTF8ToWide(url.spec());
+ disposition_ = disposition;
+ transition_ = transition;
+
+ if (command_updater_) {
+ if (!alternate_nav_url.is_valid()) {
+ command_updater_->ExecuteCommand(IDC_OPEN_CURRENT_URL);
+ } else {
+ AlternateNavURLFetcher* fetcher =
+ new AlternateNavURLFetcher(alternate_nav_url);
+ // The AlternateNavURLFetcher will listen for the pending navigation
+ // notification that will be issued as a result of the "open URL." It
+ // will automatically install itself into that navigation controller.
+ command_updater_->ExecuteCommand(IDC_OPEN_CURRENT_URL);
+ if (fetcher->state() == AlternateNavURLFetcher::NOT_STARTED) {
+ // I'm not sure this should be reachable, but I'm not also sure enough
+ // that it shouldn't to stick in a NOTREACHED(). In any case, this is
+ // harmless.
+ delete fetcher;
+ } else {
+ // The navigation controller will delete the fetcher.
+ }
+ }
+ }
+ }
+
+ if (browser_->instant())
+ browser_->instant()->DestroyPreviewContents();
+
+ update_instant_ = true;
+}
+
+void LocationBarViewMac::OnChanged() {
+ // Update the location-bar icon.
+ const int resource_id = edit_view_->GetIcon();
+ NSImage* image = AutocompleteEditViewMac::ImageForResource(resource_id);
+ location_icon_decoration_->SetImage(image);
+ ev_bubble_decoration_->SetImage(image);
+ Layout();
+
+ InstantController* instant = browser_->instant();
+ string16 suggested_text;
+ if (update_instant_ && instant && GetTabContents()) {
+ if (edit_view_->model()->user_input_in_progress() &&
+ edit_view_->model()->popup_model()->IsOpen()) {
+ instant->Update
+ (browser_->GetSelectedTabContentsWrapper(),
+ edit_view_->model()->CurrentMatch(),
+ WideToUTF16(edit_view_->GetText()),
+ edit_view_->model()->UseVerbatimInstant(),
+ &suggested_text);
+ if (!instant->IsShowingInstant())
+ edit_view_->model()->FinalizeInstantQuery(std::wstring());
+ } else {
+ instant->DestroyPreviewContents();
+ edit_view_->model()->FinalizeInstantQuery(std::wstring());
+ }
+ }
+
+ SetSuggestedText(suggested_text);
+}
+
+void LocationBarViewMac::OnSelectionBoundsChanged() {
+ NOTIMPLEMENTED();
+}
+
+void LocationBarViewMac::OnInputInProgress(bool in_progress) {
+ toolbar_model_->set_input_in_progress(in_progress);
+ Update(NULL, false);
+}
+
+void LocationBarViewMac::OnSetFocus() {
+ // Update the keyword and search hint states.
+ OnChanged();
+}
+
+void LocationBarViewMac::OnKillFocus() {
+ // Do nothing.
+}
+
+SkBitmap LocationBarViewMac::GetFavIcon() const {
+ NOTIMPLEMENTED();
+ return SkBitmap();
+}
+
+std::wstring LocationBarViewMac::GetTitle() const {
+ NOTIMPLEMENTED();
+ return std::wstring();
+}
+
+void LocationBarViewMac::Revert() {
+ edit_view_->RevertAll();
+}
+
+// TODO(pamg): Change all these, here and for other platforms, to size_t.
+int LocationBarViewMac::PageActionCount() {
+ return static_cast<int>(page_action_decorations_.size());
+}
+
+int LocationBarViewMac::PageActionVisibleCount() {
+ int result = 0;
+ for (size_t i = 0; i < page_action_decorations_.size(); ++i) {
+ if (page_action_decorations_[i]->IsVisible())
+ ++result;
+ }
+ return result;
+}
+
+TabContents* LocationBarViewMac::GetTabContents() const {
+ return browser_->GetSelectedTabContents();
+}
+
+PageActionDecoration* LocationBarViewMac::GetPageActionDecoration(
+ ExtensionAction* page_action) {
+ DCHECK(page_action);
+ for (size_t i = 0; i < page_action_decorations_.size(); ++i) {
+ if (page_action_decorations_[i]->page_action() == page_action)
+ return page_action_decorations_[i];
+ }
+ // If |page_action| is the browser action of an extension, no element in
+ // |page_action_decorations_| will match.
+ NOTREACHED();
+ return NULL;
+}
+
+void LocationBarViewMac::SetPreviewEnabledPageAction(
+ ExtensionAction* page_action, bool preview_enabled) {
+ DCHECK(page_action);
+ TabContents* contents = GetTabContents();
+ if (!contents)
+ return;
+ RefreshPageActionDecorations();
+ Layout();
+
+ PageActionDecoration* decoration = GetPageActionDecoration(page_action);
+ DCHECK(decoration);
+ if (!decoration)
+ return;
+
+ decoration->set_preview_enabled(preview_enabled);
+ decoration->UpdateVisibility(contents,
+ GURL(WideToUTF8(toolbar_model_->GetText())));
+}
+
+NSPoint LocationBarViewMac::GetPageActionBubblePoint(
+ ExtensionAction* page_action) {
+ PageActionDecoration* decoration = GetPageActionDecoration(page_action);
+ if (!decoration)
+ return NSZeroPoint;
+
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ NSRect frame = [cell frameForDecoration:decoration inFrame:[field_ bounds]];
+ DCHECK(!NSIsEmptyRect(frame));
+ if (NSIsEmptyRect(frame))
+ return NSZeroPoint;
+
+ NSPoint bubble_point = decoration->GetBubblePointInFrame(frame);
+ return [field_ convertPoint:bubble_point toView:nil];
+}
+
+NSRect LocationBarViewMac::GetBlockedPopupRect() const {
+ const size_t kPopupIndex = CONTENT_SETTINGS_TYPE_POPUPS;
+ const LocationBarDecoration* decoration =
+ content_setting_decorations_[kPopupIndex];
+ if (!decoration || !decoration->IsVisible())
+ return NSZeroRect;
+
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ const NSRect frame = [cell frameForDecoration:decoration
+ inFrame:[field_ bounds]];
+ return [field_ convertRect:frame toView:nil];
+}
+
+ExtensionAction* LocationBarViewMac::GetPageAction(size_t index) {
+ if (index < page_action_decorations_.size())
+ return page_action_decorations_[index]->page_action();
+ NOTREACHED();
+ return NULL;
+}
+
+ExtensionAction* LocationBarViewMac::GetVisiblePageAction(size_t index) {
+ size_t current = 0;
+ for (size_t i = 0; i < page_action_decorations_.size(); ++i) {
+ if (page_action_decorations_[i]->IsVisible()) {
+ if (current == index)
+ return page_action_decorations_[i]->page_action();
+
+ ++current;
+ }
+ }
+
+ NOTREACHED();
+ return NULL;
+}
+
+void LocationBarViewMac::TestPageActionPressed(size_t index) {
+ DCHECK_LT(index, page_action_decorations_.size());
+ if (index < page_action_decorations_.size())
+ page_action_decorations_[index]->OnMousePressed(NSZeroRect);
+}
+
+void LocationBarViewMac::SetEditable(bool editable) {
+ [field_ setEditable:editable ? YES : NO];
+ star_decoration_->SetVisible(browser_defaults::bookmarks_enabled &&
+ editable && !toolbar_model_->input_in_progress());
+ UpdatePageActions();
+ Layout();
+}
+
+bool LocationBarViewMac::IsEditable() {
+ return [field_ isEditable] ? true : false;
+}
+
+void LocationBarViewMac::SetStarred(bool starred) {
+ star_decoration_->SetStarred(starred);
+
+ // TODO(shess): The field-editor frame and cursor rects should not
+ // change, here.
+ [field_ updateCursorAndToolTipRects];
+ [field_ resetFieldEditorFrameIfNeeded];
+ [field_ setNeedsDisplay:YES];
+}
+
+NSPoint LocationBarViewMac::GetBookmarkBubblePoint() const {
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ const NSRect frame = [cell frameForDecoration:star_decoration_.get()
+ inFrame:[field_ bounds]];
+ const NSPoint point = star_decoration_->GetBubblePointInFrame(frame);
+ return [field_ convertPoint:point toView:nil];
+}
+
+NSPoint LocationBarViewMac::GetPageInfoBubblePoint() const {
+ AutocompleteTextFieldCell* cell = [field_ cell];
+ if (ev_bubble_decoration_->IsVisible()) {
+ const NSRect frame = [cell frameForDecoration:ev_bubble_decoration_.get()
+ inFrame:[field_ bounds]];
+ const NSPoint point = ev_bubble_decoration_->GetBubblePointInFrame(frame);
+ return [field_ convertPoint:point toView:nil];
+ } else {
+ const NSRect frame =
+ [cell frameForDecoration:location_icon_decoration_.get()
+ inFrame:[field_ bounds]];
+ const NSPoint point =
+ location_icon_decoration_->GetBubblePointInFrame(frame);
+ return [field_ convertPoint:point toView:nil];
+ }
+}
+
+NSImage* LocationBarViewMac::GetKeywordImage(const std::wstring& keyword) {
+ const TemplateURL* template_url =
+ profile_->GetTemplateURLModel()->GetTemplateURLForKeyword(keyword);
+ if (template_url && template_url->IsExtensionKeyword()) {
+ const SkBitmap& bitmap = profile_->GetExtensionsService()->
+ GetOmniboxIcon(template_url->GetExtensionId());
+ return gfx::SkBitmapToNSImage(bitmap);
+ }
+
+ return AutocompleteEditViewMac::ImageForResource(IDR_OMNIBOX_SEARCH);
+}
+
+void LocationBarViewMac::Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ switch (type.value) {
+ case NotificationType::EXTENSION_PAGE_ACTION_VISIBILITY_CHANGED: {
+ TabContents* contents = GetTabContents();
+ if (Details<TabContents>(contents) != details)
+ return;
+
+ [field_ updateCursorAndToolTipRects];
+ [field_ setNeedsDisplay:YES];
+ break;
+ }
+ default:
+ NOTREACHED() << "Unexpected notification";
+ break;
+ }
+}
+
+void LocationBarViewMac::PostNotification(NSString* notification) {
+ [[NSNotificationCenter defaultCenter] postNotificationName:notification
+ object:[NSValue valueWithPointer:this]];
+}
+
+bool LocationBarViewMac::RefreshContentSettingsDecorations() {
+ const bool input_in_progress = toolbar_model_->input_in_progress();
+ TabContents* tab_contents =
+ input_in_progress ? NULL : browser_->GetSelectedTabContents();
+ bool icons_updated = false;
+ for (size_t i = 0; i < content_setting_decorations_.size(); ++i) {
+ icons_updated |=
+ content_setting_decorations_[i]->UpdateFromTabContents(tab_contents);
+ }
+ return icons_updated;
+}
+
+void LocationBarViewMac::DeletePageActionDecorations() {
+ // TODO(shess): Deleting these decorations could result in the cell
+ // refering to them before things are laid out again. Meanwhile, at
+ // least fail safe.
+ [[field_ cell] clearDecorations];
+
+ page_action_decorations_.reset();
+}
+
+void LocationBarViewMac::RefreshPageActionDecorations() {
+ if (!IsEditable()) {
+ DeletePageActionDecorations();
+ return;
+ }
+
+ ExtensionsService* service = profile_->GetExtensionsService();
+ if (!service)
+ return;
+
+ std::vector<ExtensionAction*> page_actions;
+ for (size_t i = 0; i < service->extensions()->size(); ++i) {
+ if (service->extensions()->at(i)->page_action())
+ page_actions.push_back(service->extensions()->at(i)->page_action());
+ }
+
+ // On startup we sometimes haven't loaded any extensions. This makes sure
+ // we catch up when the extensions (and any Page Actions) load.
+ if (page_actions.size() != page_action_decorations_.size()) {
+ DeletePageActionDecorations(); // Delete the old views (if any).
+
+ for (size_t i = 0; i < page_actions.size(); ++i) {
+ page_action_decorations_.push_back(
+ new PageActionDecoration(this, profile_, page_actions[i]));
+ }
+ }
+
+ if (page_action_decorations_.empty())
+ return;
+
+ TabContents* contents = GetTabContents();
+ if (!contents)
+ return;
+
+ GURL url = GURL(WideToUTF8(toolbar_model_->GetText()));
+ for (size_t i = 0; i < page_action_decorations_.size(); ++i) {
+ page_action_decorations_[i]->UpdateVisibility(
+ toolbar_model_->input_in_progress() ? NULL : contents, url);
+ }
+}
+
+// TODO(shess): This function should over time grow to closely match
+// the views Layout() function.
+void LocationBarViewMac::Layout() {
+ AutocompleteTextFieldCell* cell = [field_ cell];
+
+ // Reset the left-hand decorations.
+ // TODO(shess): Shortly, this code will live somewhere else, like in
+ // the constructor. I am still wrestling with how best to deal with
+ // right-hand decorations, which are not a static set.
+ [cell clearDecorations];
+ [cell addLeftDecoration:location_icon_decoration_.get()];
+ [cell addLeftDecoration:selected_keyword_decoration_.get()];
+ [cell addLeftDecoration:ev_bubble_decoration_.get()];
+ [cell addRightDecoration:star_decoration_.get()];
+
+ // Note that display order is right to left.
+ for (size_t i = 0; i < page_action_decorations_.size(); ++i) {
+ [cell addRightDecoration:page_action_decorations_[i]];
+ }
+ for (size_t i = 0; i < content_setting_decorations_.size(); ++i) {
+ [cell addRightDecoration:content_setting_decorations_[i]];
+ }
+
+ [cell addRightDecoration:keyword_hint_decoration_.get()];
+
+ // By default only the location icon is visible.
+ location_icon_decoration_->SetVisible(true);
+ selected_keyword_decoration_->SetVisible(false);
+ ev_bubble_decoration_->SetVisible(false);
+ keyword_hint_decoration_->SetVisible(false);
+
+ // Get the keyword to use for keyword-search and hinting.
+ const std::wstring keyword(edit_view_->model()->keyword());
+ std::wstring short_name;
+ bool is_extension_keyword = false;
+ if (!keyword.empty()) {
+ short_name = profile_->GetTemplateURLModel()->
+ GetKeywordShortName(keyword, &is_extension_keyword);
+ }
+
+ const bool is_keyword_hint = edit_view_->model()->is_keyword_hint();
+
+ if (!keyword.empty() && !is_keyword_hint) {
+ // Switch from location icon to keyword mode.
+ location_icon_decoration_->SetVisible(false);
+ selected_keyword_decoration_->SetVisible(true);
+ selected_keyword_decoration_->SetKeyword(short_name, is_extension_keyword);
+ selected_keyword_decoration_->SetImage(GetKeywordImage(keyword));
+ } else if (toolbar_model_->GetSecurityLevel() == ToolbarModel::EV_SECURE) {
+ // Switch from location icon to show the EV bubble instead.
+ location_icon_decoration_->SetVisible(false);
+ ev_bubble_decoration_->SetVisible(true);
+
+ std::wstring label(toolbar_model_->GetEVCertName());
+ ev_bubble_decoration_->SetFullLabel(base::SysWideToNSString(label));
+ } else if (!keyword.empty() && is_keyword_hint) {
+ keyword_hint_decoration_->SetKeyword(short_name, is_extension_keyword);
+ keyword_hint_decoration_->SetVisible(true);
+ }
+
+ // These need to change anytime the layout changes.
+ // TODO(shess): Anytime the field editor might have changed, the
+ // cursor rects almost certainly should have changed. The tooltips
+ // might change even when the rects don't change.
+ [field_ resetFieldEditorFrameIfNeeded];
+ [field_ updateCursorAndToolTipRects];
+
+ [field_ setNeedsDisplay:YES];
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h b/chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h
new file mode 100644
index 0000000..920d4d3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h
@@ -0,0 +1,46 @@
+// 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_UI_COCOA_LOCATION_BAR_LOCATION_ICON_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_LOCATION_ICON_DECORATION_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/ui/cocoa/location_bar/image_decoration.h"
+
+class LocationBarViewMac;
+
+// LocationIconDecoration is used to display an icon to the left of
+// the address.
+
+class LocationIconDecoration : public ImageDecoration {
+ public:
+ explicit LocationIconDecoration(LocationBarViewMac* owner);
+ virtual ~LocationIconDecoration();
+
+ // Allow dragging the current URL.
+ virtual bool IsDraggable();
+ virtual NSPasteboard* GetDragPasteboard();
+ virtual NSImage* GetDragImage() { return GetImage(); }
+ virtual NSRect GetDragImageFrame(NSRect frame) {
+ return GetDrawRectInFrame(frame);
+ }
+
+ // Get the point where the page info bubble should point within the
+ // decoration's frame, in the |owner_|'s coordinates.
+ NSPoint GetBubblePointInFrame(NSRect frame);
+
+ // Show the page info panel on click.
+ virtual bool OnMousePressed(NSRect frame);
+ virtual bool AcceptsMousePress() { return true; }
+
+ private:
+ // The location bar view that owns us.
+ LocationBarViewMac* owner_;
+
+ DISALLOW_COPY_AND_ASSIGN(LocationIconDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_LOCATION_ICON_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/location_icon_decoration.mm b/chrome/browser/ui/cocoa/location_bar/location_icon_decoration.mm
new file mode 100644
index 0000000..808a45f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/location_icon_decoration.mm
@@ -0,0 +1,72 @@
+// 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 <cmath>
+
+#import "chrome/browser/ui/cocoa/location_bar/location_icon_decoration.h"
+
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
+#import "third_party/mozilla/NSPasteboard+Utils.h"
+
+// The info-bubble point should look like it points to the bottom of the lock
+// icon. Determined with Pixie.app.
+const CGFloat kBubblePointYOffset = 2.0;
+
+LocationIconDecoration::LocationIconDecoration(LocationBarViewMac* owner)
+ : owner_(owner) {
+}
+LocationIconDecoration::~LocationIconDecoration() {
+}
+
+bool LocationIconDecoration::IsDraggable() {
+ // Without a tab it will be impossible to get the information needed
+ // to perform a drag.
+ if (!owner_->GetTabContents())
+ return false;
+
+ // Do not drag if the user has been editing the location bar, or the
+ // location bar is at the NTP.
+ if (owner_->location_entry()->IsEditingOrEmpty())
+ return false;
+
+ return true;
+}
+
+NSPasteboard* LocationIconDecoration::GetDragPasteboard() {
+ TabContents* tab = owner_->GetTabContents();
+ DCHECK(tab); // See |IsDraggable()|.
+
+ NSString* url = base::SysUTF8ToNSString(tab->GetURL().spec());
+ NSString* title = base::SysUTF16ToNSString(tab->GetTitle());
+
+ NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
+ [pboard declareURLPasteboardWithAdditionalTypes:[NSArray array]
+ owner:nil];
+ [pboard setDataForURL:url title:title];
+ return pboard;
+}
+
+NSPoint LocationIconDecoration::GetBubblePointInFrame(NSRect frame) {
+ const NSRect draw_frame = GetDrawRectInFrame(frame);
+ return NSMakePoint(NSMidX(draw_frame),
+ NSMaxY(draw_frame) - kBubblePointYOffset);
+}
+
+bool LocationIconDecoration::OnMousePressed(NSRect frame) {
+ // Do not show page info if the user has been editing the location
+ // bar, or the location bar is at the NTP.
+ if (owner_->location_entry()->IsEditingOrEmpty())
+ return true;
+
+ TabContents* tab = owner_->GetTabContents();
+ NavigationEntry* nav_entry = tab->controller().GetActiveEntry();
+ if (!nav_entry) {
+ NOTREACHED();
+ return true;
+ }
+ tab->ShowPageInfo(nav_entry->url(), nav_entry->ssl(), true);
+ return true;
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.h b/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.h
new file mode 100644
index 0000000..c549a49
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.h
@@ -0,0 +1,17 @@
+// 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_UI_COCOA_LOCATION_BAR_OMNIBOX_POPUP_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_OMNIBOX_POPUP_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// The content view for the omnibox popup. Supports up to two subviews (the
+// AutocompleteMatrix containing autocomplete results and (optionally) an
+// InstantOptInView.
+@interface OmniboxPopupView : NSView
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_OMNIBOX_POPUP_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.mm b/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.mm
new file mode 100644
index 0000000..ef479e1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.mm
@@ -0,0 +1,43 @@
+// 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/ui/cocoa/location_bar/omnibox_popup_view.h"
+
+#include "base/logging.h"
+
+@implementation OmniboxPopupView
+
+// If there is only one subview, it is sized to fill all available space. If
+// there are two subviews, the second subview is placed at the bottom of the
+// view, and the first subview is sized to fill all remaining space.
+- (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
+ NSArray* subviews = [self subviews];
+ if ([subviews count] == 0)
+ return;
+
+ DCHECK_LE([subviews count], 2U);
+
+ NSRect availableSpace = [self bounds];
+
+ if ([subviews count] >= 2) {
+ NSView* instantView = [subviews objectAtIndex:1];
+ CGFloat height = NSHeight([instantView frame]);
+ NSRect instantFrame = availableSpace;
+ instantFrame.size.height = height;
+
+ availableSpace.origin.y = height;
+ availableSpace.size.height -= height;
+ [instantView setFrame:instantFrame];
+ }
+
+ if ([subviews count] >= 1) {
+ NSView* matrixView = [subviews objectAtIndex:0];
+ if (NSHeight(availableSpace) < 0)
+ availableSpace.size.height = 0;
+
+ [matrixView setFrame:availableSpace];
+ }
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view_unittest.mm b/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view_unittest.mm
new file mode 100644
index 0000000..ac4be55
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/omnibox_popup_view_unittest.mm
@@ -0,0 +1,68 @@
+// 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 "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.h"
+
+namespace {
+
+class OmniboxPopupViewTest : public CocoaTest {
+ public:
+ OmniboxPopupViewTest() {
+ NSRect content_frame = [[test_window() contentView] frame];
+ scoped_nsobject<OmniboxPopupView> view(
+ [[OmniboxPopupView alloc] initWithFrame:content_frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ OmniboxPopupView* view_; // Weak. Owned by the view hierarchy.
+};
+
+// Tests display, add/remove.
+TEST_VIEW(OmniboxPopupViewTest, view_);
+
+// A single subview should completely fill the popup view.
+TEST_F(OmniboxPopupViewTest, ResizeWithOneSubview) {
+ scoped_nsobject<NSView> subview1([[NSView alloc] initWithFrame:NSZeroRect]);
+
+ // Adding the subview should not change its frame.
+ [view_ addSubview:subview1];
+ EXPECT_TRUE(NSEqualRects(NSZeroRect, [subview1 frame]));
+
+ // Resizing the popup view should also resize the subview.
+ [view_ setFrame:NSMakeRect(0, 0, 100, 100)];
+ EXPECT_TRUE(NSEqualRects([view_ bounds], [subview1 frame]));
+}
+
+TEST_F(OmniboxPopupViewTest, ResizeWithTwoSubviews) {
+ const CGFloat height = 50;
+ NSRect initial = NSMakeRect(0, 0, 100, height);
+
+ scoped_nsobject<NSView> subview1([[NSView alloc] initWithFrame:NSZeroRect]);
+ scoped_nsobject<NSView> subview2([[NSView alloc] initWithFrame:initial]);
+ [view_ addSubview:subview1];
+ [view_ addSubview:subview2];
+
+ // Resize the popup view to be much larger than height. |subview2|'s height
+ // should stay the same, and |subview1| should resize to fill all available
+ // space.
+ [view_ setFrame:NSMakeRect(0, 0, 300, 4 * height)];
+ EXPECT_EQ(NSWidth([view_ frame]), NSWidth([subview1 frame]));
+ EXPECT_EQ(NSWidth([view_ frame]), NSWidth([subview2 frame]));
+ EXPECT_EQ(height, NSHeight([subview2 frame]));
+ EXPECT_EQ(NSHeight([view_ frame]),
+ NSHeight([subview1 frame]) + NSHeight([subview2 frame]));
+
+ // Now resize the popup view to be smaller than height. |subview2|'s height
+ // should stay the same, and |subview1|'s height should be zero, not negative.
+ [view_ setFrame:NSMakeRect(0, 0, 300, height - 10)];
+ EXPECT_EQ(NSWidth([view_ frame]), NSWidth([subview1 frame]));
+ EXPECT_EQ(NSWidth([view_ frame]), NSWidth([subview2 frame]));
+ EXPECT_EQ(0, NSHeight([subview1 frame]));
+ EXPECT_EQ(height, NSHeight([subview2 frame]));
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/location_bar/page_action_decoration.h b/chrome/browser/ui/cocoa/location_bar/page_action_decoration.h
new file mode 100644
index 0000000..07cd94d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/page_action_decoration.h
@@ -0,0 +1,119 @@
+// 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_UI_COCOA_LOCATION_BAR_PAGE_ACTION_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_PAGE_ACTION_DECORATION_H_
+#pragma once
+
+#include "chrome/browser/extensions/image_loading_tracker.h"
+#import "chrome/browser/ui/cocoa/location_bar/image_decoration.h"
+#include "googleurl/src/gurl.h"
+
+class ExtensionAction;
+@class ExtensionActionContextMenu;
+class LocationBarViewMac;
+class Profile;
+class TabContents;
+
+// PageActionDecoration is used to display the icon for a given Page
+// Action and notify the extension when the icon is clicked.
+
+class PageActionDecoration : public ImageDecoration,
+ public ImageLoadingTracker::Observer,
+ public NotificationObserver {
+ public:
+ PageActionDecoration(LocationBarViewMac* owner,
+ Profile* profile,
+ ExtensionAction* page_action);
+ virtual ~PageActionDecoration();
+
+ ExtensionAction* page_action() { return page_action_; }
+ int current_tab_id() { return current_tab_id_; }
+ void set_preview_enabled(bool enabled) { preview_enabled_ = enabled; }
+ bool preview_enabled() const { return preview_enabled_; }
+
+ // Overridden from |ImageLoadingTracker::Observer|.
+ virtual void OnImageLoaded(
+ SkBitmap* image, ExtensionResource resource, int index);
+
+ // Called to notify the Page Action that it should determine whether
+ // to be visible or hidden. |contents| is the TabContents that is
+ // active, |url| is the current page URL.
+ void UpdateVisibility(TabContents* contents, const GURL& url);
+
+ // Sets the tooltip for this Page Action image.
+ void SetToolTip(NSString* tooltip);
+ void SetToolTip(std::string tooltip);
+
+ // Get the point where extension info bubbles should point within
+ // the given decoration frame.
+ NSPoint GetBubblePointInFrame(NSRect frame);
+
+ // Overridden from |LocationBarDecoration|
+ virtual CGFloat GetWidthForSpace(CGFloat width);
+ virtual bool AcceptsMousePress() { return true; }
+ virtual bool OnMousePressed(NSRect frame);
+ virtual NSString* GetToolTip();
+ virtual NSMenu* GetMenu();
+
+ protected:
+ // For unit testing only.
+ PageActionDecoration() : owner_(NULL),
+ profile_(NULL),
+ page_action_(NULL),
+ tracker_(this),
+ current_tab_id_(-1),
+ preview_enabled_(false) {}
+
+ private:
+ // Overridden from NotificationObserver:
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+
+ // The location bar view that owns us.
+ LocationBarViewMac* owner_;
+
+ // The current profile (not owned by us).
+ Profile* profile_;
+
+ // The Page Action that this view represents. The Page Action is not
+ // owned by us, it resides in the extension of this particular
+ // profile.
+ ExtensionAction* page_action_;
+
+ // A cache of images the Page Actions might need to show, mapped by
+ // path.
+ typedef std::map<std::string, SkBitmap> PageActionMap;
+ PageActionMap page_action_icons_;
+
+ // The object that is waiting for the image loading to complete
+ // asynchronously.
+ ImageLoadingTracker tracker_;
+
+ // The tab id we are currently showing the icon for.
+ int current_tab_id_;
+
+ // The URL we are currently showing the icon for.
+ GURL current_url_;
+
+ // The string to show for a tooltip.
+ scoped_nsobject<NSString> tooltip_;
+
+ // The context menu for the Page Action.
+ scoped_nsobject<ExtensionActionContextMenu> menu_;
+
+ // This is used for post-install visual feedback. The page_action
+ // icon is briefly shown even if it hasn't been enabled by its
+ // extension.
+ bool preview_enabled_;
+
+ // Used to register for notifications received by
+ // NotificationObserver.
+ NotificationRegistrar registrar_;
+
+ DISALLOW_COPY_AND_ASSIGN(PageActionDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_PAGE_ACTION_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/page_action_decoration.mm b/chrome/browser/ui/cocoa/location_bar/page_action_decoration.mm
new file mode 100644
index 0000000..610815c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/page_action_decoration.mm
@@ -0,0 +1,251 @@
+// 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 <cmath>
+
+#import "chrome/browser/ui/cocoa/location_bar/page_action_decoration.h"
+
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/extensions/extension_browser_event_router.h"
+#include "chrome/browser/extensions/extensions_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#import "chrome/browser/ui/cocoa/extensions/extension_action_context_menu.h"
+#import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
+#include "chrome/common/extensions/extension_action.h"
+#include "chrome/common/extensions/extension_resource.h"
+#include "skia/ext/skia_utils_mac.h"
+
+namespace {
+
+// Distance to offset the bubble pointer from the bottom of the max
+// icon area of the decoration. This makes the popup's upper border
+// 2px away from the omnibox's lower border (matches omnibox popup
+// upper border).
+const CGFloat kBubblePointYOffset = 2.0;
+
+} // namespace
+
+PageActionDecoration::PageActionDecoration(
+ LocationBarViewMac* owner,
+ Profile* profile,
+ ExtensionAction* page_action)
+ : owner_(NULL),
+ profile_(profile),
+ page_action_(page_action),
+ tracker_(this),
+ current_tab_id_(-1),
+ preview_enabled_(false) {
+ DCHECK(profile);
+ const Extension* extension = profile->GetExtensionsService()->
+ GetExtensionById(page_action->extension_id(), false);
+ DCHECK(extension);
+
+ // Load all the icons declared in the manifest. This is the contents of the
+ // icons array, plus the default_icon property, if any.
+ std::vector<std::string> icon_paths(*page_action->icon_paths());
+ if (!page_action_->default_icon_path().empty())
+ icon_paths.push_back(page_action_->default_icon_path());
+
+ for (std::vector<std::string>::iterator iter = icon_paths.begin();
+ iter != icon_paths.end(); ++iter) {
+ tracker_.LoadImage(extension, extension->GetResource(*iter),
+ gfx::Size(Extension::kPageActionIconMaxSize,
+ Extension::kPageActionIconMaxSize),
+ ImageLoadingTracker::DONT_CACHE);
+ }
+
+ registrar_.Add(this, NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE,
+ Source<Profile>(profile_));
+
+ // We set the owner last of all so that we can determine whether we are in
+ // the process of initializing this class or not.
+ owner_ = owner;
+}
+
+PageActionDecoration::~PageActionDecoration() {}
+
+// Always |kPageActionIconMaxSize| wide. |ImageDecoration| draws the
+// image centered.
+CGFloat PageActionDecoration::GetWidthForSpace(CGFloat width) {
+ return Extension::kPageActionIconMaxSize;
+}
+
+// Either notify listeners or show a popup depending on the Page
+// Action.
+bool PageActionDecoration::OnMousePressed(NSRect frame) {
+ if (current_tab_id_ < 0) {
+ NOTREACHED() << "No current tab.";
+ // We don't want other code to try and handle this click. Returning true
+ // prevents this by indicating that we handled it.
+ return true;
+ }
+
+ if (page_action_->HasPopup(current_tab_id_)) {
+ // Anchor popup at the bottom center of the page action icon.
+ AutocompleteTextField* field = owner_->GetAutocompleteTextField();
+ NSPoint anchor = GetBubblePointInFrame(frame);
+ anchor = [field convertPoint:anchor toView:nil];
+
+ const GURL popup_url(page_action_->GetPopupUrl(current_tab_id_));
+ [ExtensionPopupController showURL:popup_url
+ inBrowser:BrowserList::GetLastActive()
+ anchoredAt:anchor
+ arrowLocation:info_bubble::kTopRight
+ devMode:NO];
+ } else {
+ ExtensionBrowserEventRouter::GetInstance()->PageActionExecuted(
+ profile_, page_action_->extension_id(), page_action_->id(),
+ current_tab_id_, current_url_.spec(),
+ 1);
+ }
+ return true;
+}
+
+void PageActionDecoration::OnImageLoaded(
+ SkBitmap* image, ExtensionResource resource, int index) {
+ // We loaded icons()->size() icons, plus one extra if the Page Action had
+ // a default icon.
+ int total_icons = static_cast<int>(page_action_->icon_paths()->size());
+ if (!page_action_->default_icon_path().empty())
+ total_icons++;
+ DCHECK(index < total_icons);
+
+ // Map the index of the loaded image back to its name. If we ever get an
+ // index greater than the number of icons, it must be the default icon.
+ if (image) {
+ if (index < static_cast<int>(page_action_->icon_paths()->size()))
+ page_action_icons_[page_action_->icon_paths()->at(index)] = *image;
+ else
+ page_action_icons_[page_action_->default_icon_path()] = *image;
+ }
+
+ // If we have no owner, that means this class is still being constructed and
+ // we should not UpdatePageActions, since it leads to the PageActions being
+ // destroyed again and new ones recreated (causing an infinite loop).
+ if (owner_)
+ owner_->UpdatePageActions();
+}
+
+void PageActionDecoration::UpdateVisibility(TabContents* contents,
+ const GURL& url) {
+ // Save this off so we can pass it back to the extension when the action gets
+ // executed. See PageActionDecoration::OnMousePressed.
+ current_tab_id_ = contents ? ExtensionTabUtil::GetTabId(contents) : -1;
+ current_url_ = url;
+
+ bool visible = contents &&
+ (preview_enabled_ || page_action_->GetIsVisible(current_tab_id_));
+ if (visible) {
+ SetToolTip(page_action_->GetTitle(current_tab_id_));
+
+ // Set the image.
+ // It can come from three places. In descending order of priority:
+ // - The developer can set it dynamically by path or bitmap. It will be in
+ // page_action_->GetIcon().
+ // - The developer can set it dynamically by index. It will be in
+ // page_action_->GetIconIndex().
+ // - It can be set in the manifest by path. It will be in page_action_->
+ // default_icon_path().
+
+ // First look for a dynamically set bitmap.
+ SkBitmap skia_icon = page_action_->GetIcon(current_tab_id_);
+ if (skia_icon.isNull()) {
+ int icon_index = page_action_->GetIconIndex(current_tab_id_);
+ std::string icon_path = (icon_index < 0) ?
+ page_action_->default_icon_path() :
+ page_action_->icon_paths()->at(icon_index);
+ if (!icon_path.empty()) {
+ PageActionMap::iterator iter = page_action_icons_.find(icon_path);
+ if (iter != page_action_icons_.end())
+ skia_icon = iter->second;
+ }
+ }
+ if (!skia_icon.isNull()) {
+ SetImage(gfx::SkBitmapToNSImage(skia_icon));
+ } else if (!GetImage()) {
+ // During install the action can be displayed before the icons
+ // have come in. Rather than deal with this in multiple places,
+ // provide a placeholder image. This will be replaced when an
+ // icon comes in.
+ const NSSize default_size = NSMakeSize(Extension::kPageActionIconMaxSize,
+ Extension::kPageActionIconMaxSize);
+ SetImage([[NSImage alloc] initWithSize:default_size]);
+ }
+ }
+
+ if (IsVisible() != visible) {
+ SetVisible(visible);
+ NotificationService::current()->Notify(
+ NotificationType::EXTENSION_PAGE_ACTION_VISIBILITY_CHANGED,
+ Source<ExtensionAction>(page_action_),
+ Details<TabContents>(contents));
+ }
+}
+
+void PageActionDecoration::SetToolTip(NSString* tooltip) {
+ tooltip_.reset([tooltip retain]);
+}
+
+void PageActionDecoration::SetToolTip(std::string tooltip) {
+ SetToolTip(tooltip.empty() ? nil : base::SysUTF8ToNSString(tooltip));
+}
+
+NSString* PageActionDecoration::GetToolTip() {
+ return tooltip_.get();
+}
+
+NSPoint PageActionDecoration::GetBubblePointInFrame(NSRect frame) {
+ // This is similar to |ImageDecoration::GetDrawRectInFrame()|,
+ // except that code centers the image, which can differ in size
+ // between actions. This centers the maximum image size, so the
+ // point will consistently be at the same y position. x position is
+ // easier (the middle of the centered image is the middle of the
+ // frame).
+ const CGFloat delta_height =
+ NSHeight(frame) - Extension::kPageActionIconMaxSize;
+ const CGFloat bottom_inset = std::ceil(delta_height / 2.0);
+
+ // Return a point just below the bottom of the maximal drawing area.
+ return NSMakePoint(NSMidX(frame),
+ NSMaxY(frame) - bottom_inset + kBubblePointYOffset);
+}
+
+NSMenu* PageActionDecoration::GetMenu() {
+ if (!profile_)
+ return nil;
+ ExtensionsService* service = profile_->GetExtensionsService();
+ if (!service)
+ return nil;
+ const Extension* extension = service->GetExtensionById(
+ page_action_->extension_id(), false);
+ DCHECK(extension);
+ if (!extension)
+ return nil;
+ menu_.reset([[ExtensionActionContextMenu alloc]
+ initWithExtension:extension
+ profile:profile_
+ extensionAction:page_action_]);
+
+ return menu_.get();
+}
+
+void PageActionDecoration::Observe(
+ NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ switch (type.value) {
+ case NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE: {
+ ExtensionPopupController* popup = [ExtensionPopupController popup];
+ if (popup && ![popup isClosing])
+ [popup close];
+
+ break;
+ }
+ default:
+ NOTREACHED() << "Unexpected notification";
+ break;
+ }
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.h b/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.h
new file mode 100644
index 0000000..3c9cf309
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.h
@@ -0,0 +1,42 @@
+// 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_UI_COCOA_LOCATION_BAR_SELECTED_KEYWORD_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_SELECTED_KEYWORD_DECORATION_H_
+#pragma once
+
+#include <string>
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/ui/cocoa/location_bar/bubble_decoration.h"
+
+class SelectedKeywordDecoration : public BubbleDecoration {
+ public:
+ SelectedKeywordDecoration(NSFont* font);
+
+ // Calculates appropriate full and partial label strings based on
+ // inputs.
+ void SetKeyword(const std::wstring& keyword, bool is_extension_keyword);
+
+ // Determines what combination of labels and image will best fit
+ // within |width|, makes those current for |BubbleDecoration|, and
+ // return the resulting width.
+ virtual CGFloat GetWidthForSpace(CGFloat width);
+
+ void SetImage(NSImage* image);
+
+ private:
+ friend class SelectedKeywordDecorationTest;
+ FRIEND_TEST_ALL_PREFIXES(SelectedKeywordDecorationTest,
+ UsesPartialKeywordIfNarrow);
+
+ scoped_nsobject<NSImage> search_image_;
+ scoped_nsobject<NSString> full_string_;
+ scoped_nsobject<NSString> partial_string_;
+
+ DISALLOW_COPY_AND_ASSIGN(SelectedKeywordDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_SELECTED_KEYWORD_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.mm b/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.mm
new file mode 100644
index 0000000..0bdd8e15
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.mm
@@ -0,0 +1,73 @@
+// 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/ui/cocoa/location_bar/selected_keyword_decoration.h"
+
+#include "app/l10n_util_mac.h"
+#include "base/utf_string_conversions.h"
+#import "chrome/browser/autocomplete/autocomplete_edit_view_mac.h"
+#include "chrome/browser/location_bar_util.h"
+#import "chrome/browser/ui/cocoa/image_utils.h"
+#include "grit/theme_resources.h"
+#include "grit/generated_resources.h"
+
+SelectedKeywordDecoration::SelectedKeywordDecoration(NSFont* font)
+ : BubbleDecoration(font) {
+ search_image_.reset([AutocompleteEditViewMac::ImageForResource(
+ IDR_KEYWORD_SEARCH_MAGNIFIER) retain]);
+
+ // Matches the color of the highlighted line in the popup.
+ NSColor* background_color = [NSColor selectedControlColor];
+
+ // Match focus ring's inner color.
+ NSColor* border_color =
+ [[NSColor keyboardFocusIndicatorColor] colorWithAlphaComponent:0.5];
+ SetColors(border_color, background_color, [NSColor blackColor]);
+}
+
+CGFloat SelectedKeywordDecoration::GetWidthForSpace(CGFloat width) {
+ const CGFloat full_width =
+ GetWidthForImageAndLabel(search_image_, full_string_);
+ if (full_width <= width) {
+ BubbleDecoration::SetImage(search_image_);
+ SetLabel(full_string_);
+ return full_width;
+ }
+
+ BubbleDecoration::SetImage(nil);
+ const CGFloat no_image_width = GetWidthForImageAndLabel(nil, full_string_);
+ if (no_image_width <= width || !partial_string_) {
+ SetLabel(full_string_);
+ return no_image_width;
+ }
+
+ SetLabel(partial_string_);
+ return GetWidthForImageAndLabel(nil, partial_string_);
+}
+
+void SelectedKeywordDecoration::SetKeyword(const std::wstring& short_name,
+ bool is_extension_keyword) {
+ const std::wstring min_name(
+ location_bar_util::CalculateMinString(short_name));
+ const int message_id = is_extension_keyword ?
+ IDS_OMNIBOX_EXTENSION_KEYWORD_TEXT : IDS_OMNIBOX_KEYWORD_TEXT;
+
+ // The text will be like "Search <name>:". "<name>" is a parameter
+ // derived from |short_name|.
+ full_string_.reset(
+ [l10n_util::GetNSStringF(message_id, WideToUTF16(short_name)) copy]);
+
+ if (min_name.empty()) {
+ partial_string_.reset();
+ } else {
+ partial_string_.reset(
+ [l10n_util::GetNSStringF(message_id, WideToUTF16(min_name)) copy]);
+ }
+}
+
+void SelectedKeywordDecoration::SetImage(NSImage* image) {
+ if (image != search_image_)
+ search_image_.reset([image retain]);
+ BubbleDecoration::SetImage(image);
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration_unittest.mm b/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration_unittest.mm
new file mode 100644
index 0000000..5536fda
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration_unittest.mm
@@ -0,0 +1,64 @@
+// 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/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/location_bar/selected_keyword_decoration.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+
+namespace {
+
+// A wide width which should fit everything.
+const CGFloat kWidth(300.0);
+
+// A narrow width for tests which test things that don't fit.
+const CGFloat kNarrowWidth(5.0);
+
+} // namespace
+
+class SelectedKeywordDecorationTest : public CocoaTest {
+ public:
+ SelectedKeywordDecorationTest()
+ : decoration_([NSFont userFontOfSize:12]) {
+ }
+
+ SelectedKeywordDecoration decoration_;
+};
+
+// Test that the cell correctly chooses the partial keyword if there's
+// not enough room.
+TEST_F(SelectedKeywordDecorationTest, UsesPartialKeywordIfNarrow) {
+
+ const std::wstring kKeyword(L"Engine");
+ NSString* const kFullString = @"Search Engine:";
+ NSString* const kPartialString = @"Search En\u2026:"; // ellipses
+
+ decoration_.SetKeyword(kKeyword, false);
+
+ // Wide width chooses the full string and image.
+ const CGFloat all_width = decoration_.GetWidthForSpace(kWidth);
+ EXPECT_TRUE(decoration_.image_);
+ EXPECT_NSEQ(kFullString, decoration_.label_);
+
+ // If not enough space to include the image, uses exactly the full
+ // string.
+ const CGFloat full_width = decoration_.GetWidthForSpace(all_width - 5.0);
+ EXPECT_LT(full_width, all_width);
+ EXPECT_FALSE(decoration_.image_);
+ EXPECT_NSEQ(kFullString, decoration_.label_);
+
+ // Narrow width chooses the partial string.
+ const CGFloat partial_width = decoration_.GetWidthForSpace(kNarrowWidth);
+ EXPECT_LT(partial_width, full_width);
+ EXPECT_FALSE(decoration_.image_);
+ EXPECT_NSEQ(kPartialString, decoration_.label_);
+
+ // Narrow doesn't choose partial string if there is not one.
+ decoration_.partial_string_.reset();
+ decoration_.GetWidthForSpace(kNarrowWidth);
+ EXPECT_FALSE(decoration_.image_);
+ EXPECT_NSEQ(kFullString, decoration_.label_);
+}
diff --git a/chrome/browser/ui/cocoa/location_bar/star_decoration.h b/chrome/browser/ui/cocoa/location_bar/star_decoration.h
new file mode 100644
index 0000000..0d12104
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/star_decoration.h
@@ -0,0 +1,44 @@
+// 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_UI_COCOA_LOCATION_BAR_STAR_DECORATION_H_
+#define CHROME_BROWSER_UI_COCOA_LOCATION_BAR_STAR_DECORATION_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/ui/cocoa/location_bar/image_decoration.h"
+
+class CommandUpdater;
+
+// Star icon on the right side of the field.
+
+class StarDecoration : public ImageDecoration {
+ public:
+ explicit StarDecoration(CommandUpdater* command_updater);
+ virtual ~StarDecoration();
+
+ // Sets the image and tooltip based on |starred|.
+ void SetStarred(bool starred);
+
+ // Get the point where the bookmark bubble should point within the
+ // decoration's frame.
+ NSPoint GetBubblePointInFrame(NSRect frame);
+
+ // Implement |LocationBarDecoration|.
+ virtual bool AcceptsMousePress() { return true; }
+ virtual bool OnMousePressed(NSRect frame);
+ virtual NSString* GetToolTip();
+
+ private:
+ // For bringing up bookmark bar.
+ CommandUpdater* command_updater_; // Weak, owned by Browser.
+
+ // The string to show for a tooltip.
+ scoped_nsobject<NSString> tooltip_;
+
+ DISALLOW_COPY_AND_ASSIGN(StarDecoration);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_LOCATION_BAR_STAR_DECORATION_H_
diff --git a/chrome/browser/ui/cocoa/location_bar/star_decoration.mm b/chrome/browser/ui/cocoa/location_bar/star_decoration.mm
new file mode 100644
index 0000000..2ac3450
--- /dev/null
+++ b/chrome/browser/ui/cocoa/location_bar/star_decoration.mm
@@ -0,0 +1,53 @@
+// 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/ui/cocoa/location_bar/star_decoration.h"
+
+#include "app/l10n_util_mac.h"
+#include "chrome/app/chrome_command_ids.h"
+#import "chrome/browser/autocomplete/autocomplete_edit_view_mac.h"
+#include "chrome/browser/command_updater.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+
+namespace {
+
+// The info-bubble point should look like it points to the point
+// between the star's lower tips. The popup should be where the
+// Omnibox popup ends up (2px below field). Determined via Pixie.app
+// magnification.
+const CGFloat kStarPointYOffset = 2.0;
+
+} // namespace
+
+StarDecoration::StarDecoration(CommandUpdater* command_updater)
+ : command_updater_(command_updater) {
+ SetVisible(true);
+ SetStarred(false);
+}
+
+StarDecoration::~StarDecoration() {
+}
+
+void StarDecoration::SetStarred(bool starred) {
+ const int image_id = starred ? IDR_STAR_LIT : IDR_STAR;
+ const int tip_id = starred ? IDS_TOOLTIP_STARRED : IDS_TOOLTIP_STAR;
+ SetImage(AutocompleteEditViewMac::ImageForResource(image_id));
+ tooltip_.reset([l10n_util::GetNSStringWithFixup(tip_id) retain]);
+}
+
+NSPoint StarDecoration::GetBubblePointInFrame(NSRect frame) {
+ const NSRect draw_frame = GetDrawRectInFrame(frame);
+ return NSMakePoint(NSMidX(draw_frame),
+ NSMaxY(draw_frame) - kStarPointYOffset);
+}
+
+bool StarDecoration::OnMousePressed(NSRect frame) {
+ command_updater_->ExecuteCommand(IDC_BOOKMARK_PAGE);
+ return true;
+}
+
+NSString* StarDecoration::GetToolTip() {
+ return tooltip_.get();
+}
diff --git a/chrome/browser/ui/cocoa/menu_button.h b/chrome/browser/ui/cocoa/menu_button.h
new file mode 100644
index 0000000..d7b00f5
--- /dev/null
+++ b/chrome/browser/ui/cocoa/menu_button.h
@@ -0,0 +1,32 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_MENU_BUTTON_H_
+#define CHROME_BROWSER_UI_COCOA_MENU_BUTTON_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+
+// This a button which displays a user-provided menu "attached" below it upon
+// being clicked or dragged (or clicked and held). It expects a
+// |ClickHoldButtonCell| as cell.
+@interface MenuButton : NSButton {
+ @private
+ IBOutlet NSMenu* attachedMenu_;
+ scoped_nsobject<NSPopUpButtonCell> popUpCell_;
+}
+
+// The menu to display. Note that it should have no (i.e., a blank) title and
+// that the 0-th entry should be blank (and won't be displayed). (This is
+// because we use a pulldown list, for which Cocoa uses the 0-th item as "title"
+// in the button. This might change if we ever switch to a pop-up. Our direct
+// use of the given NSMenu object means that the one can set and use NSMenu's
+// delegate as usual.)
+@property(assign, nonatomic) NSMenu* attachedMenu;
+
+@end // @interface MenuButton
+
+#endif // CHROME_BROWSER_UI_COCOA_MENU_BUTTON_H_
diff --git a/chrome/browser/ui/cocoa/menu_button.mm b/chrome/browser/ui/cocoa/menu_button.mm
new file mode 100644
index 0000000..d1b9e88
--- /dev/null
+++ b/chrome/browser/ui/cocoa/menu_button.mm
@@ -0,0 +1,122 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/menu_button.h"
+
+#include "base/logging.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/clickhold_button_cell.h"
+
+@interface MenuButton (Private)
+
+- (void)resetToDefaults;
+- (void)showMenu:(BOOL)isDragging;
+- (void)clickShowMenu:(id)sender;
+- (void)dragShowMenu:(id)sender;
+
+@end // @interface MenuButton (Private)
+
+@implementation MenuButton
+
+// Overrides:
+
++ (Class)cellClass {
+ return [ClickHoldButtonCell class];
+}
+
+- (id)init {
+ if ((self = [super init]))
+ [self resetToDefaults];
+ return self;
+}
+
+- (id)initWithCoder:(NSCoder*)decoder {
+ if ((self = [super initWithCoder:decoder]))
+ [self resetToDefaults];
+ return self;
+}
+
+- (id)initWithFrame:(NSRect)frameRect {
+ if ((self = [super initWithFrame:frameRect]))
+ [self resetToDefaults];
+ return self;
+}
+
+// Accessors and mutators:
+
+@synthesize attachedMenu = attachedMenu_;
+
+@end // @implementation MenuButton
+
+@implementation MenuButton (Private)
+
+// Reset various settings of the button and its associated |ClickHoldButtonCell|
+// to the standard state which provides reasonable defaults.
+- (void)resetToDefaults {
+ ClickHoldButtonCell* cell = [self cell];
+ DCHECK([cell isKindOfClass:[ClickHoldButtonCell class]]);
+ [cell setEnableClickHold:YES];
+ [cell setClickHoldTimeout:0.0]; // Make menu trigger immediately.
+ [cell setAction:@selector(clickShowMenu:)];
+ [cell setTarget:self];
+ [cell setClickHoldAction:@selector(dragShowMenu:)];
+ [cell setClickHoldTarget:self];
+}
+
+// Actually show the menu (in the correct location). |isDragging| indicates
+// whether the mouse button is still down or not.
+- (void)showMenu:(BOOL)isDragging {
+ if (![self attachedMenu]) {
+ LOG(WARNING) << "No menu available.";
+ if (isDragging) {
+ // If we're dragging, wait for mouse up.
+ [NSApp nextEventMatchingMask:NSLeftMouseUpMask
+ untilDate:[NSDate distantFuture]
+ inMode:NSEventTrackingRunLoopMode
+ dequeue:YES];
+ }
+ return;
+ }
+
+ // TODO(viettrungluu): Remove silly fudge factors (same ones as in
+ // delayedmenu_button.mm).
+ NSRect frame = [self convertRect:[self frame]
+ fromView:[self superview]];
+ frame.origin.x -= 2.0;
+ frame.size.height += 10.0;
+
+ // Make our pop-up button cell and set things up. This is, as of 10.5, the
+ // official Apple-recommended hack. Later, perhaps |-[NSMenu
+ // popUpMenuPositioningItem:atLocation:inView:]| may be a better option.
+ // However, using a pulldown has the benefit that Cocoa automatically places
+ // the menu correctly even when we're at the edge of the screen (including
+ // "dragging upwards" when the button is close to the bottom of the screen).
+ // A |scoped_nsobject| local variable cannot be used here because
+ // Accessibility on 10.5 grabs the NSPopUpButtonCell without retaining it, and
+ // uses it later. (This is fixed in 10.6.)
+ if (!popUpCell_.get()) {
+ popUpCell_.reset([[NSPopUpButtonCell alloc] initTextCell:@""
+ pullsDown:YES]);
+ }
+ DCHECK(popUpCell_.get());
+ [popUpCell_ setMenu:[self attachedMenu]];
+ [popUpCell_ selectItem:nil];
+ [popUpCell_ attachPopUpWithFrame:frame
+ inView:self];
+ [popUpCell_ performClickWithFrame:frame
+ inView:self];
+}
+
+// Called when the button is clicked and released. (Shouldn't happen with
+// timeout of 0, though there may be some strange pointing devices out there.)
+- (void)clickShowMenu:(id)sender {
+ [self showMenu:NO];
+}
+
+// Called when the button is clicked and dragged/held.
+- (void)dragShowMenu:(id)sender {
+ [self showMenu:YES];
+}
+
+@end // @implementation MenuButton (Private)
diff --git a/chrome/browser/ui/cocoa/menu_button_unittest.mm b/chrome/browser/ui/cocoa/menu_button_unittest.mm
new file mode 100644
index 0000000..ccfcb2c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/menu_button_unittest.mm
@@ -0,0 +1,50 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/clickhold_button_cell.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/menu_button.h"
+
+namespace {
+
+class MenuButtonTest : public CocoaTest {
+ public:
+ MenuButtonTest() {
+ NSRect frame = NSMakeRect(0, 0, 50, 30);
+ scoped_nsobject<MenuButton> button(
+ [[MenuButton alloc] initWithFrame:frame]);
+ button_ = button.get();
+ scoped_nsobject<ClickHoldButtonCell> cell(
+ [[ClickHoldButtonCell alloc] initTextCell:@"Testing"]);
+ [button_ setCell:cell.get()];
+ [[test_window() contentView] addSubview:button_];
+ }
+
+ MenuButton* button_;
+};
+
+TEST_VIEW(MenuButtonTest, button_);
+
+// Test assigning a menu, again mostly to ensure nothing leaks or crashes.
+TEST_F(MenuButtonTest, MenuAssign) {
+ scoped_nsobject<NSMenu> menu([[NSMenu alloc] initWithTitle:@""]);
+ ASSERT_TRUE(menu.get());
+
+ [menu insertItemWithTitle:@"" action:nil keyEquivalent:@"" atIndex:0];
+ [menu insertItemWithTitle:@"foo" action:nil keyEquivalent:@"" atIndex:1];
+ [menu insertItemWithTitle:@"bar" action:nil keyEquivalent:@"" atIndex:2];
+ [menu insertItemWithTitle:@"baz" action:nil keyEquivalent:@"" atIndex:3];
+
+ [button_ setAttachedMenu:menu];
+ EXPECT_TRUE([button_ attachedMenu]);
+
+ // TODO(viettrungluu): Display the menu. (The tough part is closing the menu,
+ // not opening it!)
+
+ // Since |button_| doesn't retain menu, we should probably unset it here.
+ [button_ setAttachedMenu:nil];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/menu_controller.h b/chrome/browser/ui/cocoa/menu_controller.h
new file mode 100644
index 0000000..c198c70
--- /dev/null
+++ b/chrome/browser/ui/cocoa/menu_controller.h
@@ -0,0 +1,67 @@
+// 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_UI_COCOA_MENU_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_MENU_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+
+namespace menus {
+class MenuModel;
+}
+
+// A controller for the cross-platform menu model. The menu that's created
+// has the tag and represented object set for each menu item. The object is a
+// NSValue holding a pointer to the model for that level of the menu (to
+// allow for hierarchical menus). The tag is the index into that model for
+// that particular item. It is important that the model outlives this object
+// as it only maintains weak references.
+@interface MenuController : NSObject {
+ @protected
+ menus::MenuModel* model_; // weak
+ scoped_nsobject<NSMenu> menu_;
+ BOOL useWithPopUpButtonCell_; // If YES, 0th item is blank
+}
+
+@property (nonatomic, assign) menus::MenuModel* model;
+// Note that changing this will have no effect if you use
+// |-initWithModel:useWithPopUpButtonCell:| or after the first call to |-menu|.
+@property (nonatomic) BOOL useWithPopUpButtonCell;
+
+// NIB-based initializer. This does not create a menu. Clients can set the
+// properties of the object and the menu will be created upon the first call to
+// |-menu|. Note that the menu will be immutable after creation.
+- (id)init;
+
+// Builds a NSMenu from the pre-built model (must not be nil). Changes made
+// to the contents of the model after calling this will not be noticed. If
+// the menu will be displayed by a NSPopUpButtonCell, it needs to be of a
+// slightly different form (0th item is empty). Note this attribute of the menu
+// cannot be changed after it has been created.
+- (id)initWithModel:(menus::MenuModel*)model
+ useWithPopUpButtonCell:(BOOL)useWithCell;
+
+// Access to the constructed menu if the complex initializer was used. If the
+// default initializer was used, then this will create the menu on first call.
+- (NSMenu*)menu;
+
+@end
+
+// Exposed only for unit testing, do not call directly.
+@interface MenuController (PrivateExposedForTesting)
+- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item;
+@end
+
+// Protected methods that subclassers can override.
+@interface MenuController (Protected)
+- (void)addItemToMenu:(NSMenu*)menu
+ atIndex:(NSInteger)index
+ fromModel:(menus::MenuModel*)model
+ modelIndex:(int)modelIndex;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_MENU_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/menu_controller.mm b/chrome/browser/ui/cocoa/menu_controller.mm
new file mode 100644
index 0000000..47f0c34
--- /dev/null
+++ b/chrome/browser/ui/cocoa/menu_controller.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/ui/cocoa/menu_controller.h"
+
+#include "app/l10n_util_mac.h"
+#include "app/menus/accelerator_cocoa.h"
+#include "app/menus/simple_menu_model.h"
+#include "base/logging.h"
+#include "base/sys_string_conversions.h"
+#include "skia/ext/skia_utils_mac.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+@interface MenuController (Private)
+- (NSMenu*)menuFromModel:(menus::MenuModel*)model;
+- (void)addSeparatorToMenu:(NSMenu*)menu
+ atIndex:(int)index;
+@end
+
+@implementation MenuController
+
+@synthesize model = model_;
+@synthesize useWithPopUpButtonCell = useWithPopUpButtonCell_;
+
+- (id)init {
+ self = [super init];
+ return self;
+}
+
+- (id)initWithModel:(menus::MenuModel*)model
+ useWithPopUpButtonCell:(BOOL)useWithCell {
+ if ((self = [super init])) {
+ model_ = model;
+ useWithPopUpButtonCell_ = useWithCell;
+ [self menu];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ model_ = NULL;
+ [super dealloc];
+}
+
+// Creates a NSMenu from the given model. If the model has submenus, this can
+// be invoked recursively.
+- (NSMenu*)menuFromModel:(menus::MenuModel*)model {
+ NSMenu* menu = [[[NSMenu alloc] initWithTitle:@""] autorelease];
+
+ // The indices may not always start at zero (the windows system menu is one
+ // example where this is used) so just make sure we can handle it.
+ // SimpleMenuModel currently always starts at 0.
+ int firstItemIndex = model->GetFirstItemIndex(menu);
+ DCHECK(firstItemIndex == 0);
+ const int count = model->GetItemCount();
+ for (int index = firstItemIndex; index < firstItemIndex + count; index++) {
+ int modelIndex = index - firstItemIndex;
+ if (model->GetTypeAt(modelIndex) == menus::MenuModel::TYPE_SEPARATOR) {
+ [self addSeparatorToMenu:menu atIndex:index];
+ } else {
+ [self addItemToMenu:menu atIndex:index fromModel:model
+ modelIndex:modelIndex];
+ }
+ }
+
+ return menu;
+}
+
+// Adds a separator item at the given index. As the separator doesn't need
+// anything from the model, this method doesn't need the model index as the
+// other method below does.
+- (void)addSeparatorToMenu:(NSMenu*)menu
+ atIndex:(int)index {
+ NSMenuItem* separator = [NSMenuItem separatorItem];
+ [menu insertItem:separator atIndex:index];
+}
+
+// Adds an item or a hierarchical menu to the item at the |index|,
+// associated with the entry in the model indentifed by |modelIndex|.
+- (void)addItemToMenu:(NSMenu*)menu
+ atIndex:(NSInteger)index
+ fromModel:(menus::MenuModel*)model
+ modelIndex:(int)modelIndex {
+ NSString* label =
+ l10n_util::FixUpWindowsStyleLabel(model->GetLabelAt(modelIndex));
+ scoped_nsobject<NSMenuItem> item(
+ [[NSMenuItem alloc] initWithTitle:label
+ action:@selector(itemSelected:)
+ keyEquivalent:@""]);
+
+ // If the menu item has an icon, set it.
+ SkBitmap skiaIcon;
+ if (model->GetIconAt(modelIndex, &skiaIcon) && !skiaIcon.isNull()) {
+ NSImage* icon = gfx::SkBitmapToNSImage(skiaIcon);
+ if (icon) {
+ [item setImage:icon];
+ }
+ }
+
+ menus::MenuModel::ItemType type = model->GetTypeAt(modelIndex);
+ if (type == menus::MenuModel::TYPE_SUBMENU) {
+ // Recursively build a submenu from the sub-model at this index.
+ [item setTarget:nil];
+ [item setAction:nil];
+ menus::MenuModel* submenuModel = model->GetSubmenuModelAt(modelIndex);
+ NSMenu* submenu =
+ [self menuFromModel:(menus::SimpleMenuModel*)submenuModel];
+ [item setSubmenu:submenu];
+ } else {
+ // The MenuModel works on indexes so we can't just set the command id as the
+ // tag like we do in other menus. Also set the represented object to be
+ // the model so hierarchical menus check the correct index in the correct
+ // model. Setting the target to |self| allows this class to participate
+ // in validation of the menu items.
+ [item setTag:modelIndex];
+ [item setTarget:self];
+ NSValue* modelObject = [NSValue valueWithPointer:model];
+ [item setRepresentedObject:modelObject]; // Retains |modelObject|.
+ menus::AcceleratorCocoa accelerator;
+ if (model->GetAcceleratorAt(modelIndex, &accelerator)) {
+ [item setKeyEquivalent:accelerator.characters()];
+ [item setKeyEquivalentModifierMask:accelerator.modifiers()];
+ }
+ }
+ [menu insertItem:item atIndex:index];
+}
+
+// Called before the menu is to be displayed to update the state (enabled,
+// radio, etc) of each item in the menu. Also will update the title if
+// the item is marked as "dynamic".
+- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
+ SEL action = [item action];
+ if (action != @selector(itemSelected:))
+ return NO;
+
+ NSInteger modelIndex = [item tag];
+ menus::MenuModel* model =
+ static_cast<menus::MenuModel*>(
+ [[(id)item representedObject] pointerValue]);
+ DCHECK(model);
+ if (model) {
+ BOOL checked = model->IsItemCheckedAt(modelIndex);
+ DCHECK([(id)item isKindOfClass:[NSMenuItem class]]);
+ [(id)item setState:(checked ? NSOnState : NSOffState)];
+ [(id)item setHidden:(!model->IsVisibleAt(modelIndex))];
+ if (model->IsLabelDynamicAt(modelIndex)) {
+ NSString* label =
+ l10n_util::FixUpWindowsStyleLabel(model->GetLabelAt(modelIndex));
+ [(id)item setTitle:label];
+ }
+ return model->IsEnabledAt(modelIndex);
+ }
+ return NO;
+}
+
+// Called when the user chooses a particular menu item. |sender| is the menu
+// item chosen.
+- (void)itemSelected:(id)sender {
+ NSInteger modelIndex = [sender tag];
+ menus::MenuModel* model =
+ static_cast<menus::MenuModel*>(
+ [[sender representedObject] pointerValue]);
+ DCHECK(model);
+ if (model)
+ model->ActivatedAt(modelIndex);
+}
+
+- (NSMenu*)menu {
+ if (!menu_ && model_) {
+ menu_.reset([[self menuFromModel:model_] retain]);
+ // If this is to be used with a NSPopUpButtonCell, add an item at the 0th
+ // position that's empty. Doing it after the menu has been constructed won't
+ // complicate creation logic, and since the tags are model indexes, they
+ // are unaffected by the extra item.
+ if (useWithPopUpButtonCell_) {
+ scoped_nsobject<NSMenuItem> blankItem(
+ [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]);
+ [menu_ insertItem:blankItem atIndex:0];
+ }
+ }
+ return menu_.get();
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/menu_controller_unittest.mm b/chrome/browser/ui/cocoa/menu_controller_unittest.mm
new file mode 100644
index 0000000..9171adf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/menu_controller_unittest.mm
@@ -0,0 +1,197 @@
+// 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 "app/menus/simple_menu_model.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/menu_controller.h"
+#include "grit/generated_resources.h"
+
+class MenuControllerTest : public CocoaTest {
+};
+
+// A menu delegate that counts the number of times certain things are called
+// to make sure things are hooked up properly.
+class Delegate : public menus::SimpleMenuModel::Delegate {
+ public:
+ Delegate() : execute_count_(0), enable_count_(0) { }
+
+ virtual bool IsCommandIdChecked(int command_id) const { return false; }
+ virtual bool IsCommandIdEnabled(int command_id) const {
+ ++enable_count_;
+ return true;
+ }
+ virtual bool GetAcceleratorForCommandId(
+ int command_id,
+ menus::Accelerator* accelerator) { return false; }
+ virtual void ExecuteCommand(int command_id) { ++execute_count_; }
+
+ int execute_count_;
+ mutable int enable_count_;
+};
+
+TEST_F(MenuControllerTest, EmptyMenu) {
+ Delegate delegate;
+ menus::SimpleMenuModel model(&delegate);
+ scoped_nsobject<MenuController> menu(
+ [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
+ EXPECT_EQ([[menu menu] numberOfItems], 0);
+}
+
+TEST_F(MenuControllerTest, BasicCreation) {
+ Delegate delegate;
+ menus::SimpleMenuModel model(&delegate);
+ model.AddItem(1, ASCIIToUTF16("one"));
+ model.AddItem(2, ASCIIToUTF16("two"));
+ model.AddItem(3, ASCIIToUTF16("three"));
+ model.AddSeparator();
+ model.AddItem(4, ASCIIToUTF16("four"));
+ model.AddItem(5, ASCIIToUTF16("five"));
+
+ scoped_nsobject<MenuController> menu(
+ [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
+ EXPECT_EQ([[menu menu] numberOfItems], 6);
+
+ // Check the title, tag, and represented object are correct for a random
+ // element.
+ NSMenuItem* itemTwo = [[menu menu] itemAtIndex:2];
+ NSString* title = [itemTwo title];
+ EXPECT_EQ(ASCIIToUTF16("three"), base::SysNSStringToUTF16(title));
+ EXPECT_EQ([itemTwo tag], 2);
+ EXPECT_EQ([[itemTwo representedObject] pointerValue], &model);
+
+ EXPECT_TRUE([[[menu menu] itemAtIndex:3] isSeparatorItem]);
+}
+
+TEST_F(MenuControllerTest, Submenus) {
+ Delegate delegate;
+ menus::SimpleMenuModel model(&delegate);
+ model.AddItem(1, ASCIIToUTF16("one"));
+ menus::SimpleMenuModel submodel(&delegate);
+ submodel.AddItem(2, ASCIIToUTF16("sub-one"));
+ submodel.AddItem(3, ASCIIToUTF16("sub-two"));
+ submodel.AddItem(4, ASCIIToUTF16("sub-three"));
+ model.AddSubMenuWithStringId(5, IDS_ZOOM_MENU, &submodel);
+ model.AddItem(6, ASCIIToUTF16("three"));
+
+ scoped_nsobject<MenuController> menu(
+ [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
+ EXPECT_EQ([[menu menu] numberOfItems], 3);
+
+ // Inspect the submenu to ensure it has correct properties.
+ NSMenu* submenu = [[[menu menu] itemAtIndex:1] submenu];
+ EXPECT_TRUE(submenu);
+ EXPECT_EQ([submenu numberOfItems], 3);
+
+ // Inspect one of the items to make sure it has the correct model as its
+ // represented object and the proper tag.
+ NSMenuItem* submenuItem = [submenu itemAtIndex:1];
+ NSString* title = [submenuItem title];
+ EXPECT_EQ(ASCIIToUTF16("sub-two"), base::SysNSStringToUTF16(title));
+ EXPECT_EQ([submenuItem tag], 1);
+ EXPECT_EQ([[submenuItem representedObject] pointerValue], &submodel);
+
+ // Make sure the item after the submenu is correct and its represented
+ // object is back to the top model.
+ NSMenuItem* item = [[menu menu] itemAtIndex:2];
+ title = [item title];
+ EXPECT_EQ(ASCIIToUTF16("three"), base::SysNSStringToUTF16(title));
+ EXPECT_EQ([item tag], 2);
+ EXPECT_EQ([[item representedObject] pointerValue], &model);
+}
+
+TEST_F(MenuControllerTest, EmptySubmenu) {
+ Delegate delegate;
+ menus::SimpleMenuModel model(&delegate);
+ model.AddItem(1, ASCIIToUTF16("one"));
+ menus::SimpleMenuModel submodel(&delegate);
+ model.AddSubMenuWithStringId(2, IDS_ZOOM_MENU, &submodel);
+
+ scoped_nsobject<MenuController> menu(
+ [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
+ EXPECT_EQ([[menu menu] numberOfItems], 2);
+}
+
+TEST_F(MenuControllerTest, PopUpButton) {
+ Delegate delegate;
+ menus::SimpleMenuModel model(&delegate);
+ model.AddItem(1, ASCIIToUTF16("one"));
+ model.AddItem(2, ASCIIToUTF16("two"));
+ model.AddItem(3, ASCIIToUTF16("three"));
+
+ // Menu should have an extra item inserted at position 0 that has an empty
+ // title.
+ scoped_nsobject<MenuController> menu(
+ [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:YES]);
+ EXPECT_EQ([[menu menu] numberOfItems], 4);
+ EXPECT_EQ(base::SysNSStringToUTF16([[[menu menu] itemAtIndex:0] title]),
+ string16());
+
+ // Make sure the tags are still correct (the index no longer matches the tag).
+ NSMenuItem* itemTwo = [[menu menu] itemAtIndex:2];
+ EXPECT_EQ([itemTwo tag], 1);
+}
+
+TEST_F(MenuControllerTest, Execute) {
+ Delegate delegate;
+ menus::SimpleMenuModel model(&delegate);
+ model.AddItem(1, ASCIIToUTF16("one"));
+ scoped_nsobject<MenuController> menu(
+ [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
+ EXPECT_EQ([[menu menu] numberOfItems], 1);
+
+ // Fake selecting the menu item, we expect the delegate to be told to execute
+ // a command.
+ NSMenuItem* item = [[menu menu] itemAtIndex:0];
+ [[item target] performSelector:[item action] withObject:item];
+ EXPECT_EQ(delegate.execute_count_, 1);
+}
+
+void Validate(MenuController* controller, NSMenu* menu) {
+ for (int i = 0; i < [menu numberOfItems]; ++i) {
+ NSMenuItem* item = [menu itemAtIndex:i];
+ [controller validateUserInterfaceItem:item];
+ if ([item hasSubmenu])
+ Validate(controller, [item submenu]);
+ }
+}
+
+TEST_F(MenuControllerTest, Validate) {
+ Delegate delegate;
+ menus::SimpleMenuModel model(&delegate);
+ model.AddItem(1, ASCIIToUTF16("one"));
+ model.AddItem(2, ASCIIToUTF16("two"));
+ menus::SimpleMenuModel submodel(&delegate);
+ submodel.AddItem(2, ASCIIToUTF16("sub-one"));
+ model.AddSubMenuWithStringId(3, IDS_ZOOM_MENU, &submodel);
+
+ scoped_nsobject<MenuController> menu(
+ [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
+ EXPECT_EQ([[menu menu] numberOfItems], 3);
+
+ Validate(menu.get(), [menu menu]);
+}
+
+TEST_F(MenuControllerTest, DefaultInitializer) {
+ Delegate delegate;
+ menus::SimpleMenuModel model(&delegate);
+ model.AddItem(1, ASCIIToUTF16("one"));
+ model.AddItem(2, ASCIIToUTF16("two"));
+ model.AddItem(3, ASCIIToUTF16("three"));
+
+ scoped_nsobject<MenuController> menu([[MenuController alloc] init]);
+ EXPECT_FALSE([menu menu]);
+
+ [menu setModel:&model];
+ [menu setUseWithPopUpButtonCell:NO];
+ EXPECT_TRUE([menu menu]);
+ EXPECT_EQ(3, [[menu menu] numberOfItems]);
+
+ // Check immutability.
+ model.AddItem(4, ASCIIToUTF16("four"));
+ EXPECT_EQ(3, [[menu menu] numberOfItems]);
+}
diff --git a/chrome/browser/ui/cocoa/menu_tracked_button.h b/chrome/browser/ui/cocoa/menu_tracked_button.h
new file mode 100644
index 0000000..3ac9bd0
--- /dev/null
+++ b/chrome/browser/ui/cocoa/menu_tracked_button.h
@@ -0,0 +1,43 @@
+// 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_UI_COCOA_MENU_TRACKED_BUTTON_H_
+#define CHROME_BROWSER_UI_COCOA_MENU_TRACKED_BUTTON_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// A MenuTrackedButton is meant to be used whenever a button is placed inside
+// the custom view of an NSMenuItem. If the user opens the menu in a non-sticky
+// fashion (i.e. clicks, holds, and drags) and then releases the mouse over
+// a MenuTrackedButton, it will |-performClick:| itself.
+//
+// To create the hover state effects, there are two code paths. When the menu
+// is opened sticky, a tracking rect produces mouse entered/exit events that
+// allow for setting the cell's highlight property. When in a drag cycle,
+// however, the only event received is |-mouseDragged:|. Therefore, a
+// delayed selector is scheduled to poll the mouse location after each drag
+// event. This checks if the user is still over the button after the drag
+// events stop being sent, indicating either the user is hovering without
+// movement or that the mouse is no longer over the receiver.
+@interface MenuTrackedButton : NSButton {
+ @private
+ // If the button received a |-mouseEntered:| event. This short-circuits the
+ // custom drag tracking logic.
+ BOOL didEnter_;
+
+ // Whether or not the user is in a click-drag-release event sequence. If so
+ // and this receives a |-mouseUp:|, then this will click itself.
+ BOOL tracking_;
+
+ // In order to get hover effects when the menu is sticky-opened, a tracking
+ // rect needs to be installed on the button.
+ NSTrackingRectTag trackingTag_;
+}
+
+@property (nonatomic, readonly, getter=isTracking) BOOL tracking;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_MENU_TRACKED_BUTTON_H_
diff --git a/chrome/browser/ui/cocoa/menu_tracked_button.mm b/chrome/browser/ui/cocoa/menu_tracked_button.mm
new file mode 100644
index 0000000..37a79bb
--- /dev/null
+++ b/chrome/browser/ui/cocoa/menu_tracked_button.mm
@@ -0,0 +1,118 @@
+// 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/ui/cocoa/menu_tracked_button.h"
+
+#include <cmath>
+
+@interface MenuTrackedButton (Private)
+- (void)doHighlight:(BOOL)highlight;
+- (void)checkMouseInRect;
+- (NSRect)insetBounds;
+- (BOOL)shouldHighlightOnHover;
+@end
+
+@implementation MenuTrackedButton
+
+@synthesize tracking = tracking_;
+
+- (void)updateTrackingAreas {
+ [super updateTrackingAreas];
+ [self removeTrackingRect:trackingTag_];
+ trackingTag_ = [self addTrackingRect:NSInsetRect([self bounds], 1, 1)
+ owner:self
+ userData:NULL
+ assumeInside:NO];
+}
+
+- (void)viewDidMoveToWindow {
+ [self updateTrackingAreas];
+ [self doHighlight:NO];
+}
+
+- (void)mouseEntered:(NSEvent*)theEvent {
+ if (!tracking_) {
+ didEnter_ = YES;
+ }
+ [self doHighlight:YES];
+ [super mouseEntered:theEvent];
+}
+
+- (void)mouseExited:(NSEvent*)theEvent {
+ didEnter_ = NO;
+ tracking_ = NO;
+ [self doHighlight:NO];
+ [super mouseExited:theEvent];
+}
+
+- (void)mouseDragged:(NSEvent*)theEvent {
+ tracking_ = !didEnter_;
+
+ NSPoint point = [self convertPoint:[theEvent locationInWindow] fromView:nil];
+ BOOL highlight = NSPointInRect(point, [self insetBounds]);
+ [self doHighlight:highlight];
+
+ // If tracking in non-sticky mode, poll the mouse cursor to see if it is still
+ // over the button and thus needs to be highlighted. The delay is the
+ // smallest that still produces the effect while minimizing jank. Smaller
+ // values make the selector fire too close to immediately/now for the mouse to
+ // have moved off the receiver, and larger values produce lag.
+ if (tracking_ && [self shouldHighlightOnHover]) {
+ [self performSelector:@selector(checkMouseInRect)
+ withObject:nil
+ afterDelay:0.05
+ inModes:[NSArray arrayWithObject:NSEventTrackingRunLoopMode]];
+ }
+ [super mouseDragged:theEvent];
+}
+
+- (void)mouseUp:(NSEvent*)theEvent {
+ [self doHighlight:NO];
+ if (!tracking_) {
+ return [super mouseUp:theEvent];
+ }
+ [self performClick:self];
+ tracking_ = NO;
+}
+
+- (void)doHighlight:(BOOL)highlight {
+ if (![self shouldHighlightOnHover]) {
+ return;
+ }
+ [[self cell] setHighlighted:highlight];
+ [self setNeedsDisplay];
+}
+
+// Checks if the user's current mouse location is over this button. If it is,
+// the user is merely hovering here. If it is not, then disable the highlight.
+// If the menu is opened in non-sticky mode, the button does not receive enter/
+// exit mouse events and thus polling is necessary.
+- (void)checkMouseInRect {
+ NSPoint point = [NSEvent mouseLocation];
+ point = [[self window] convertScreenToBase:point];
+ point = [self convertPoint:point fromView:nil];
+ if (!NSPointInRect(point, [self insetBounds])) {
+ [self doHighlight:NO];
+ }
+}
+
+// Returns the bounds of the receiver slightly inset to avoid highlighting both
+// buttons in a pair that overlap.
+- (NSRect)insetBounds {
+ return NSInsetRect([self bounds], 2, 1);
+}
+
+- (BOOL)shouldHighlightOnHover {
+ // Apple does not define NSAppKitVersionNumber10_5 when using the 10.5 SDK.
+ // The Internets have come up with this solution.
+ #ifndef NSAppKitVersionNumber10_5
+ #define NSAppKitVersionNumber10_5 949
+ #endif
+
+ // There's a cell drawing bug in 10.5 that was fixed on 10.6. Hover states
+ // look terrible due to this, so disable highlighting on 10.5.
+ return std::floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_5;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/menu_tracked_button_unittest.mm b/chrome/browser/ui/cocoa/menu_tracked_button_unittest.mm
new file mode 100644
index 0000000..9b6f77a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/menu_tracked_button_unittest.mm
@@ -0,0 +1,117 @@
+// 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/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/menu_tracked_button.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+// This test does not test what you'd think it does. Testing around event
+// tracking run loops is probably not worh the effort when the size of the
+// helper MakeEvent() is larger than the class being tested. If we ever figure
+// out a good way to test event tracking, this should be revisited.
+
+@interface MenuTrackedButtonTestReceiver : NSObject {
+ @public
+ BOOL didThat_;
+}
+- (void)doThat:(id)sender;
+@end
+@implementation MenuTrackedButtonTestReceiver
+- (void)doThat:(id)sender {
+ didThat_ = YES;
+}
+@end
+
+
+class MenuTrackedButtonTest : public CocoaTest {
+ public:
+ MenuTrackedButtonTest() : event_number_(0) {}
+
+ void SetUp() {
+ listener_.reset([[MenuTrackedButtonTestReceiver alloc] init]);
+ button_.reset(
+ [[MenuTrackedButton alloc] initWithFrame:NSMakeRect(10, 10, 50, 50)]);
+ [[test_window() contentView] addSubview:button()];
+ [button_ setTarget:listener()];
+ [button_ setAction:@selector(doThat:)];
+ }
+
+ // Creates an event of |type|, with |location| in test_window()'s coordinates.
+ NSEvent* MakeEvent(NSEventType type, NSPoint location) {
+ NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
+ location = [test_window() convertBaseToScreen:location];
+ if (type == NSMouseEntered || type == NSMouseExited) {
+ return [NSEvent enterExitEventWithType:type
+ location:location
+ modifierFlags:0
+ timestamp:now
+ windowNumber:[test_window() windowNumber]
+ context:nil
+ eventNumber:event_number_++
+ trackingNumber:0
+ userData:nil];
+ } else {
+ return [NSEvent mouseEventWithType:type
+ location:location
+ modifierFlags:0
+ timestamp:now
+ windowNumber:[test_window() windowNumber]
+ context:nil
+ eventNumber:event_number_++
+ clickCount:1
+ pressure:1.0];
+ }
+ }
+
+ MenuTrackedButtonTestReceiver* listener() { return listener_.get(); }
+ NSButton* button() { return button_.get(); }
+
+ scoped_nsobject<MenuTrackedButtonTestReceiver> listener_;
+ scoped_nsobject<MenuTrackedButton> button_;
+ NSInteger event_number_;
+};
+
+// User mouses over and then off.
+TEST_F(MenuTrackedButtonTest, DISABLED_EnterExit) {
+ [NSApp postEvent:MakeEvent(NSMouseEntered, NSMakePoint(11, 11)) atStart:YES];
+ [NSApp postEvent:MakeEvent(NSMouseExited, NSMakePoint(9, 9)) atStart:YES];
+ EXPECT_FALSE(listener()->didThat_);
+}
+
+// User mouses over, clicks, drags, and exits.
+TEST_F(MenuTrackedButtonTest, DISABLED_EnterDragExit) {
+ [NSApp postEvent:MakeEvent(NSMouseEntered, NSMakePoint(11, 11)) atStart:YES];
+ [NSApp postEvent:MakeEvent(NSLeftMouseDown, NSMakePoint(12, 12)) atStart:YES];
+ [NSApp postEvent:MakeEvent(NSLeftMouseDragged, NSMakePoint(13, 11))
+ atStart:YES];
+ [NSApp postEvent:MakeEvent(NSLeftMouseDragged, NSMakePoint(13, 10))
+ atStart:YES];
+ [NSApp postEvent:MakeEvent(NSMouseExited, NSMakePoint(13, 9)) atStart:YES];
+ EXPECT_FALSE(listener()->didThat_);
+}
+
+// User mouses over, clicks, drags, and releases.
+TEST_F(MenuTrackedButtonTest, DISABLED_EnterDragUp) {
+ [NSApp postEvent:MakeEvent(NSMouseEntered, NSMakePoint(11, 11)) atStart:YES];
+ [NSApp postEvent:MakeEvent(NSLeftMouseDown, NSMakePoint(12, 12)) atStart:YES];
+ [NSApp postEvent:MakeEvent(NSLeftMouseDragged, NSMakePoint(13, 13))
+ atStart:YES];
+ [NSApp postEvent:MakeEvent(NSLeftMouseUp, NSMakePoint(14, 14)) atStart:YES];
+ EXPECT_TRUE(listener()->didThat_);
+}
+
+// User drags in and releases.
+TEST_F(MenuTrackedButtonTest, DISABLED_DragUp) {
+ [NSApp postEvent:MakeEvent(NSLeftMouseDragged, NSMakePoint(11, 11))
+ atStart:YES];
+ [NSApp postEvent:MakeEvent(NSLeftMouseDragged, NSMakePoint(12, 12))
+ atStart:YES];
+ [NSApp postEvent:MakeEvent(NSLeftMouseUp, NSMakePoint(13, 13))
+ atStart:YES];
+ EXPECT_TRUE(listener()->didThat_);
+}
diff --git a/chrome/browser/ui/cocoa/menu_tracked_root_view.h b/chrome/browser/ui/cocoa/menu_tracked_root_view.h
new file mode 100644
index 0000000..c475783
--- /dev/null
+++ b/chrome/browser/ui/cocoa/menu_tracked_root_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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_MENU_TRACKED_ROOT_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_MENU_TRACKED_ROOT_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// An instance of MenuTrackedRootView should be the root of the view hierarchy
+// of the custom view of NSMenuItems. If the user opens the menu in a non-
+// sticky fashion (i.e. clicks, holds, and drags) and then releases the mouse
+// over the menu item, it will cancel tracking on the |[menuItem_ menu]|.
+@interface MenuTrackedRootView : NSView {
+ @private
+ // The menu item whose custom view's root view is an instance of this class.
+ NSMenuItem* menuItem_; // weak
+}
+
+@property (assign, nonatomic) NSMenuItem* menuItem;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_MENU_TRACKED_ROOT_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/menu_tracked_root_view.mm b/chrome/browser/ui/cocoa/menu_tracked_root_view.mm
new file mode 100644
index 0000000..29dd93d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/menu_tracked_root_view.mm
@@ -0,0 +1,15 @@
+// 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/ui/cocoa/menu_tracked_root_view.h"
+
+@implementation MenuTrackedRootView
+
+@synthesize menuItem = menuItem_;
+
+- (void)mouseUp:(NSEvent*)theEvent {
+ [[menuItem_ menu] cancelTracking];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/menu_tracked_root_view_unittest.mm b/chrome/browser/ui/cocoa/menu_tracked_root_view_unittest.mm
new file mode 100644
index 0000000..cb3eab1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/menu_tracked_root_view_unittest.mm
@@ -0,0 +1,45 @@
+// 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/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/menu_tracked_root_view.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+
+class MenuTrackedRootViewTest : public CocoaTest {
+ public:
+ void SetUp() {
+ CocoaTest::SetUp();
+ view_.reset([[MenuTrackedRootView alloc] init]);
+ }
+
+ scoped_nsobject<MenuTrackedRootView> view_;
+};
+
+TEST_F(MenuTrackedRootViewTest, MouseUp) {
+ id menu = [OCMockObject mockForClass:[NSMenu class]];
+ [[menu expect] cancelTracking];
+
+ id menuItem = [OCMockObject mockForClass:[NSMenuItem class]];
+ [[[menuItem stub] andReturn:menu] menu];
+
+ [view_ setMenuItem:menuItem];
+ NSEvent* event = [NSEvent mouseEventWithType:NSLeftMouseUp
+ location:NSMakePoint(42, 42)
+ modifierFlags:0
+ timestamp:0
+ windowNumber:[test_window() windowNumber]
+ context:nil
+ eventNumber:1
+ clickCount:1
+ pressure:1.0];
+ [view_ mouseUp:event];
+
+ [menu verify];
+ [menuItem verify];
+}
diff --git a/chrome/browser/ui/cocoa/multi_key_equivalent_button.h b/chrome/browser/ui/cocoa/multi_key_equivalent_button.h
new file mode 100644
index 0000000..d87c739
--- /dev/null
+++ b/chrome/browser/ui/cocoa/multi_key_equivalent_button.h
@@ -0,0 +1,36 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_MULTI_KEY_EQUIVALENT_BUTTON_H_
+#define CHROME_BROWSER_UI_COCOA_MULTI_KEY_EQUIVALENT_BUTTON_H_
+#pragma once
+
+#import <AppKit/AppKit.h>
+
+#include <vector>
+
+struct KeyEquivalentAndModifierMask {
+ public:
+ KeyEquivalentAndModifierMask() : charCode(nil), mask(0) {}
+ NSString* charCode;
+ NSUInteger mask;
+};
+
+// MultiKeyEquivalentButton is an NSButton subclass that is capable of
+// responding to additional key equivalents. It will respond to the ordinary
+// NSButton key equivalent set by -setKeyEquivalent: and
+// -setKeyEquivalentModifierMask:, and it will also respond to any additional
+// equivalents provided to it in a KeyEquivalentAndModifierMask structure
+// passed to -addKeyEquivalent:.
+
+@interface MultiKeyEquivalentButton : NSButton {
+ @private
+ std::vector<KeyEquivalentAndModifierMask> extraKeys_;
+}
+
+- (void)addKeyEquivalent:(KeyEquivalentAndModifierMask)key;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_MULTI_KEY_EQUIVALENT_BUTTON_H_
diff --git a/chrome/browser/ui/cocoa/multi_key_equivalent_button.mm b/chrome/browser/ui/cocoa/multi_key_equivalent_button.mm
new file mode 100644
index 0000000..cfe0a69
--- /dev/null
+++ b/chrome/browser/ui/cocoa/multi_key_equivalent_button.mm
@@ -0,0 +1,33 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/multi_key_equivalent_button.h"
+
+@implementation MultiKeyEquivalentButton
+
+- (void)addKeyEquivalent:(KeyEquivalentAndModifierMask)key {
+ extraKeys_.push_back(key);
+}
+
+- (BOOL)performKeyEquivalent:(NSEvent*)event {
+ NSWindow* modalWindow = [NSApp modalWindow];
+ NSWindow* window = [self window];
+
+ if ([self isEnabled] &&
+ (!modalWindow || modalWindow == window || [window worksWhenModal])) {
+ for (size_t index = 0; index < extraKeys_.size(); ++index) {
+ KeyEquivalentAndModifierMask key = extraKeys_[index];
+ if (key.charCode &&
+ [key.charCode isEqualToString:[event charactersIgnoringModifiers]] &&
+ ([event modifierFlags] & key.mask) == key.mask) {
+ [self performClick:self];
+ return YES;
+ }
+ }
+ }
+
+ return [super performKeyEquivalent:event];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/new_tab_button.h b/chrome/browser/ui/cocoa/new_tab_button.h
new file mode 100644
index 0000000..9578cbf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/new_tab_button.h
@@ -0,0 +1,28 @@
+// 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_UI_COCOA_NEW_TAB_BUTTON
+#define CHROME_BROWSER_UI_COCOA_NEW_TAB_BUTTON
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+
+// Overrides hit-test behavior to only accept clicks inside the image of the
+// button, not just inside the bounding box. This could be abstracted to general
+// use, but no other buttons are so irregularly shaped with respect to their
+// bounding box.
+
+@interface NewTabButton : NSButton {
+ @private
+ scoped_nsobject<NSBezierPath> imagePath_;
+}
+
+// Returns YES if the given point is over the button. |point| is in the
+// superview's coordinate system.
+- (BOOL)pointIsOverButton:(NSPoint)point;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_NEW_TAB_BUTTON
diff --git a/chrome/browser/ui/cocoa/new_tab_button.mm b/chrome/browser/ui/cocoa/new_tab_button.mm
new file mode 100644
index 0000000..6a97d3b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/new_tab_button.mm
@@ -0,0 +1,42 @@
+// 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/ui/cocoa/new_tab_button.h"
+
+@implementation NewTabButton
+
+// Approximate the shape. It doesn't need to be perfect. This will need to be
+// updated if the size or shape of the icon ever changes.
+// TODO(pinkerton): use a click mask image instead of hard-coding points.
+- (NSBezierPath*)pathForButton {
+ if (imagePath_.get())
+ return imagePath_.get();
+
+ // Cache the path as it doesn't change (the coordinates are local to this
+ // view). There's not much point making constants for these, as they are
+ // custom.
+ imagePath_.reset([[NSBezierPath bezierPath] retain]);
+ [imagePath_ moveToPoint:NSMakePoint(9, 7)];
+ [imagePath_ lineToPoint:NSMakePoint(26, 7)];
+ [imagePath_ lineToPoint:NSMakePoint(33, 23)];
+ [imagePath_ lineToPoint:NSMakePoint(14, 23)];
+ [imagePath_ lineToPoint:NSMakePoint(9, 7)];
+ return imagePath_;
+}
+
+- (BOOL)pointIsOverButton:(NSPoint)point {
+ NSPoint localPoint = [self convertPoint:point fromView:[self superview]];
+ NSBezierPath* buttonPath = [self pathForButton];
+ return [buttonPath containsPoint:localPoint];
+}
+
+// Override to only accept clicks within the bounds of the defined path, not
+// the entire bounding box. |aPoint| is in the superview's coordinate system.
+- (NSView*)hitTest:(NSPoint)aPoint {
+ if ([self pointIsOverButton:aPoint])
+ return [super hitTest:aPoint];
+ return nil;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/notifications/balloon_controller.h b/chrome/browser/ui/cocoa/notifications/balloon_controller.h
new file mode 100644
index 0000000..c60b9b9
--- /dev/null
+++ b/chrome/browser/ui/cocoa/notifications/balloon_controller.h
@@ -0,0 +1,98 @@
+// 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_UI_COCOA_NOTIFICATIONS_BALLOON_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_NOTIFICATIONS_BALLOON_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+
+class Balloon;
+@class BalloonContentViewCocoa;
+@class BalloonShelfViewCocoa;
+class BalloonViewHost;
+@class HoverImageButton;
+@class MenuController;
+class NotificationOptionsMenuModel;
+
+// The Balloon controller creates the view elements to display a
+// notification balloon, resize it if the HTML contents of that
+// balloon change, and move it when the collection of balloons is
+// modified.
+@interface BalloonController : NSWindowController<NSWindowDelegate> {
+ @private
+ // The balloon which represents the contents of this view. Weak pointer
+ // owned by the browser's NotificationUIManager.
+ Balloon* balloon_;
+
+ // The view that contains the contents of the notification
+ IBOutlet BalloonContentViewCocoa* htmlContainer_;
+
+ // The view that contains the controls of the notification
+ IBOutlet BalloonShelfViewCocoa* shelf_;
+
+ // The close button.
+ IBOutlet NSButton* closeButton_;
+
+ // Tracking region for the close button.
+ int closeButtonTrackingTag_;
+
+ // The origin label.
+ IBOutlet NSTextField* originLabel_;
+
+ // The options menu that appears when "options" is pressed.
+ IBOutlet HoverImageButton* optionsButton_;
+ scoped_ptr<NotificationOptionsMenuModel> menuModel_;
+ scoped_nsobject<MenuController> menuController_;
+
+ // The host for the renderer of the HTML contents.
+ scoped_ptr<BalloonViewHost> htmlContents_;
+
+ // The psn of the front application process.
+ ProcessSerialNumber frontProcessNum_;
+}
+
+// Initialize with a balloon object containing the notification data.
+- (id)initWithBalloon:(Balloon*)balloon;
+
+// Callback function for the close button.
+- (IBAction)closeButtonPressed:(id)sender;
+
+// Callback function for the options button.
+- (IBAction)optionsButtonPressed:(id)sender;
+
+// Callback function for the "revoke" option in the menu.
+- (IBAction)permissionRevoked:(id)sender;
+
+// Closes the balloon. Can be called by the bridge or by the close
+// button handler.
+- (void)closeBalloon:(bool)byUser;
+
+// Update the contents of the balloon to match the notification.
+- (void)updateContents;
+
+// Repositions the view to match the position and size of the balloon.
+// Called by the bridge when the size changes.
+- (void)repositionToBalloon;
+
+// The current size of the view, possibly subject to an animation completing.
+- (int)desiredTotalWidth;
+- (int)desiredTotalHeight;
+
+// The BalloonHost
+- (BalloonViewHost*)getHost;
+
+// Handle the event if it is for the balloon.
+- (BOOL)handleEvent:(NSEvent*)event;
+@end
+
+@interface BalloonController (UnitTesting)
+- (void)initializeHost;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_NOTIFICATIONS_BALLOON_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/notifications/balloon_controller.mm b/chrome/browser/ui/cocoa/notifications/balloon_controller.mm
new file mode 100644
index 0000000..2a80512
--- /dev/null
+++ b/chrome/browser/ui/cocoa/notifications/balloon_controller.mm
@@ -0,0 +1,241 @@
+// 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 "chrome/browser/ui/cocoa/notifications/balloon_controller.h"
+
+#include "app/l10n_util.h"
+#include "app/resource_bundle.h"
+#import "base/cocoa_protocols_mac.h"
+#include "base/mac_util.h"
+#include "base/nsimage_cache_mac.h"
+#import "base/scoped_nsobject.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/notifications/balloon.h"
+#include "chrome/browser/notifications/desktop_notification_service.h"
+#include "chrome/browser/notifications/notification.h"
+#include "chrome/browser/notifications/notification_options_menu_model.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/renderer_host/render_view_host.h"
+#import "chrome/browser/ui/cocoa/hover_image_button.h"
+#import "chrome/browser/ui/cocoa/menu_controller.h"
+#import "chrome/browser/ui/cocoa/notifications/balloon_view.h"
+#include "chrome/browser/ui/cocoa/notifications/balloon_view_host_mac.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+
+namespace {
+
+// Margin, in pixels, between the notification frame and the contents
+// of the notification.
+const int kTopMargin = 1;
+const int kBottomMargin = 2;
+const int kLeftMargin = 2;
+const int kRightMargin = 2;
+
+} // namespace
+
+@interface BalloonController (Private)
+- (void)updateTrackingRect;
+@end
+
+@implementation BalloonController
+
+- (id)initWithBalloon:(Balloon*)balloon {
+ NSString* nibpath =
+ [mac_util::MainAppBundle() pathForResource:@"Notification"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ balloon_ = balloon;
+ [self initializeHost];
+ menuModel_.reset(new NotificationOptionsMenuModel(balloon));
+ menuController_.reset([[MenuController alloc] initWithModel:menuModel_.get()
+ useWithPopUpButtonCell:NO]);
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ DCHECK([self window]);
+ DCHECK_EQ(self, [[self window] delegate]);
+
+ NSImage* image = nsimage_cache::ImageNamed(@"balloon_wrench.pdf");
+ [optionsButton_ setDefaultImage:image];
+ [optionsButton_ setDefaultOpacity:0.6];
+ [optionsButton_ setHoverImage:image];
+ [optionsButton_ setHoverOpacity:0.9];
+ [optionsButton_ setPressedImage:image];
+ [optionsButton_ setPressedOpacity:1.0];
+ [[optionsButton_ cell] setHighlightsBy:NSNoCellMask];
+
+ NSString* sourceLabelText = l10n_util::GetNSStringF(
+ IDS_NOTIFICATION_BALLOON_SOURCE_LABEL,
+ balloon_->notification().display_source());
+ [originLabel_ setStringValue:sourceLabelText];
+
+ // This condition is false in unit tests which have no RVH.
+ if (htmlContents_.get()) {
+ gfx::NativeView contents = htmlContents_->native_view();
+ [contents setFrame:NSMakeRect(kLeftMargin, kTopMargin, 0, 0)];
+ [[htmlContainer_ superview] addSubview:contents
+ positioned:NSWindowBelow
+ relativeTo:nil];
+ }
+
+ // Use the standard close button for a utility window.
+ closeButton_ = [NSWindow standardWindowButton:NSWindowCloseButton
+ forStyleMask:NSUtilityWindowMask];
+ NSRect frame = [closeButton_ frame];
+ [closeButton_ setFrame:NSMakeRect(6, 1, frame.size.width, frame.size.height)];
+ [closeButton_ setTarget:self];
+ [closeButton_ setAction:@selector(closeButtonPressed:)];
+ [shelf_ addSubview:closeButton_];
+ [self updateTrackingRect];
+
+ // Set the initial position without animating (the balloon should not
+ // yet be visible).
+ DCHECK(![[self window] isVisible]);
+ NSRect balloon_frame = NSMakeRect(balloon_->GetPosition().x(),
+ balloon_->GetPosition().y(),
+ [self desiredTotalWidth],
+ [self desiredTotalHeight]);
+ [[self window] setFrame:balloon_frame
+ display:NO];
+}
+
+- (void)updateTrackingRect {
+ if (closeButtonTrackingTag_)
+ [shelf_ removeTrackingRect:closeButtonTrackingTag_];
+
+ closeButtonTrackingTag_ = [shelf_ addTrackingRect:[closeButton_ frame]
+ owner:self
+ userData:nil
+ assumeInside:NO];
+}
+
+- (BOOL)handleEvent:(NSEvent*)event {
+ BOOL eventHandled = NO;
+ if ([event type] == NSLeftMouseDown) {
+ NSPoint mouse = [shelf_ convertPoint:[event locationInWindow]
+ fromView:nil];
+ if (NSPointInRect(mouse, [closeButton_ frame])) {
+ [closeButton_ mouseDown:event];
+
+ // Bring back the front process that is deactivated when we click the
+ // close button.
+ if (frontProcessNum_.highLongOfPSN || frontProcessNum_.lowLongOfPSN) {
+ SetFrontProcessWithOptions(&frontProcessNum_,
+ kSetFrontProcessFrontWindowOnly);
+ frontProcessNum_.highLongOfPSN = 0;
+ frontProcessNum_.lowLongOfPSN = 0;
+ }
+
+ eventHandled = YES;
+ } else if (NSPointInRect(mouse, [optionsButton_ frame])) {
+ [optionsButton_ mouseDown:event];
+ eventHandled = YES;
+ }
+ }
+ return eventHandled;
+}
+
+- (void) mouseEntered:(NSEvent*)event {
+ [[closeButton_ cell] setHighlighted:YES];
+
+ // Remember the current front process so that we can bring it back later.
+ if (!frontProcessNum_.highLongOfPSN && !frontProcessNum_.lowLongOfPSN)
+ GetFrontProcess(&frontProcessNum_);
+}
+
+- (void) mouseExited:(NSEvent*)event {
+ [[closeButton_ cell] setHighlighted:NO];
+
+ frontProcessNum_.highLongOfPSN = 0;
+ frontProcessNum_.lowLongOfPSN = 0;
+}
+
+- (IBAction)optionsButtonPressed:(id)sender {
+ [NSMenu popUpContextMenu:[menuController_ menu]
+ withEvent:[NSApp currentEvent]
+ forView:optionsButton_];
+}
+
+- (IBAction)permissionRevoked:(id)sender {
+ DesktopNotificationService* service =
+ balloon_->profile()->GetDesktopNotificationService();
+ service->DenyPermission(balloon_->notification().origin_url());
+}
+
+- (IBAction)closeButtonPressed:(id)sender {
+ [self closeBalloon:YES];
+ [self close];
+}
+
+- (void)close {
+ if (closeButtonTrackingTag_)
+ [shelf_ removeTrackingRect:closeButtonTrackingTag_];
+
+ [super close];
+}
+
+- (void)closeBalloon:(bool)byUser {
+ if (!balloon_)
+ return;
+ [self close];
+ if (htmlContents_.get())
+ htmlContents_->Shutdown();
+ if (balloon_)
+ balloon_->OnClose(byUser);
+ balloon_ = NULL;
+}
+
+- (void)updateContents {
+ DCHECK(htmlContents_.get()) << "BalloonView::Update called before Show";
+ if (htmlContents_->render_view_host())
+ htmlContents_->render_view_host()->NavigateToURL(
+ balloon_->notification().content_url());
+}
+
+- (void)repositionToBalloon {
+ DCHECK(balloon_);
+ int x = balloon_->GetPosition().x();
+ int y = balloon_->GetPosition().y();
+ int w = [self desiredTotalWidth];
+ int h = [self desiredTotalHeight];
+
+ if (htmlContents_.get())
+ htmlContents_->UpdateActualSize(balloon_->content_size());
+
+ [[[self window] animator] setFrame:NSMakeRect(x, y, w, h)
+ display:YES];
+}
+
+// Returns the total width the view should be to accommodate the balloon.
+- (int)desiredTotalWidth {
+ return (balloon_ ? balloon_->content_size().width() : 0) +
+ kLeftMargin + kRightMargin;
+}
+
+// Returns the total height the view should be to accommodate the balloon.
+- (int)desiredTotalHeight {
+ return (balloon_ ? balloon_->content_size().height() : 0) +
+ kTopMargin + kBottomMargin + [shelf_ frame].size.height;
+}
+
+// Returns the BalloonHost {
+- (BalloonViewHost*) getHost {
+ return htmlContents_.get();
+}
+
+// Initializes the renderer host showing the HTML contents.
+- (void)initializeHost {
+ htmlContents_.reset(new BalloonViewHost(balloon_));
+ htmlContents_->Init();
+}
+
+// NSWindowDelegate notification.
+- (void)windowWillClose:(NSNotification*)notif {
+ [self autorelease];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/notifications/balloon_controller_unittest.mm b/chrome/browser/ui/cocoa/notifications/balloon_controller_unittest.mm
new file mode 100644
index 0000000..6cedbd4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/notifications/balloon_controller_unittest.mm
@@ -0,0 +1,112 @@
+// 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"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/notifications/balloon.h"
+#include "chrome/browser/notifications/balloon_collection.h"
+#include "chrome/browser/notifications/notification.h"
+#include "chrome/browser/renderer_host/test/test_render_view_host.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/notifications/balloon_controller.h"
+#include "chrome/test/testing_profile.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+
+// Subclass balloon controller and mock out the initialization of the RVH.
+@interface TestBalloonController : BalloonController {
+}
+- (void)initializeHost;
+@end
+
+@implementation TestBalloonController
+- (void)initializeHost {}
+@end
+
+namespace {
+
+// Use a dummy balloon collection for testing.
+class MockBalloonCollection : public BalloonCollection {
+ virtual void Add(const Notification& notification,
+ Profile* profile) {}
+ virtual bool RemoveById(const std::string& id) { return false; }
+ virtual bool RemoveBySourceOrigin(const GURL& origin) { return false; }
+ virtual bool HasSpace() const { return true; }
+ virtual void ResizeBalloon(Balloon* balloon, const gfx::Size& size) {};
+ virtual void DisplayChanged() {}
+ virtual void OnBalloonClosed(Balloon* source) {};
+ virtual const Balloons& GetActiveBalloons() {
+ NOTREACHED();
+ return balloons_;
+ }
+ private:
+ Balloons balloons_;
+};
+
+class BalloonControllerTest : public RenderViewHostTestHarness {
+ public:
+ BalloonControllerTest() :
+ ui_thread_(BrowserThread::UI, MessageLoop::current()),
+ io_thread_(BrowserThread::IO, MessageLoop::current()) {
+ }
+
+ virtual void SetUp() {
+ RenderViewHostTestHarness::SetUp();
+ CocoaTest::BootstrapCocoa();
+ profile_.reset(new TestingProfile());
+ profile_->CreateRequestContext();
+ browser_.reset(new Browser(Browser::TYPE_NORMAL, profile_.get()));
+ collection_.reset(new MockBalloonCollection());
+ }
+
+ virtual void TearDown() {
+ MessageLoop::current()->RunAllPending();
+ RenderViewHostTestHarness::TearDown();
+ }
+
+ protected:
+ BrowserThread ui_thread_;
+ BrowserThread io_thread_;
+ scoped_ptr<TestingProfile> profile_;
+ scoped_ptr<Browser> browser_;
+ scoped_ptr<BalloonCollection> collection_;
+};
+
+TEST_F(BalloonControllerTest, ShowAndCloseTest) {
+ Notification n(GURL("http://www.google.com"), GURL("http://www.google.com"),
+ ASCIIToUTF16("http://www.google.com"), string16(),
+ new NotificationObjectProxy(-1, -1, -1, false));
+ scoped_ptr<Balloon> balloon(
+ new Balloon(n, profile_.get(), collection_.get()));
+ balloon->SetPosition(gfx::Point(1, 1), false);
+ balloon->set_content_size(gfx::Size(100, 100));
+
+ BalloonController* controller =
+ [[TestBalloonController alloc] initWithBalloon:balloon.get()];
+
+ [controller showWindow:nil];
+ [controller closeBalloon:YES];
+}
+
+TEST_F(BalloonControllerTest, SizesTest) {
+ Notification n(GURL("http://www.google.com"), GURL("http://www.google.com"),
+ ASCIIToUTF16("http://www.google.com"), string16(),
+ new NotificationObjectProxy(-1, -1, -1, false));
+ scoped_ptr<Balloon> balloon(
+ new Balloon(n, profile_.get(), collection_.get()));
+ balloon->SetPosition(gfx::Point(1, 1), false);
+ balloon->set_content_size(gfx::Size(100, 100));
+
+ BalloonController* controller =
+ [[TestBalloonController alloc] initWithBalloon:balloon.get()];
+
+ [controller showWindow:nil];
+
+ EXPECT_TRUE([controller desiredTotalWidth] > 100);
+ EXPECT_TRUE([controller desiredTotalHeight] > 100);
+
+ [controller closeBalloon:YES];
+}
+
+}
diff --git a/chrome/browser/ui/cocoa/notifications/balloon_view.h b/chrome/browser/ui/cocoa/notifications/balloon_view.h
new file mode 100644
index 0000000..b742eaf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/notifications/balloon_view.h
@@ -0,0 +1,28 @@
+// 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_UI_COCOA_NOTIFICATIONS_BALLOON_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_NOTIFICATIONS_BALLOON_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+@interface BalloonWindow : NSWindow {
+}
+@end
+
+// This view class draws a frame around the HTML contents of a
+// notification balloon.
+@interface BalloonContentViewCocoa : NSView {
+}
+@end
+
+// This view class draws the shelf of a notification balloon,
+// containing the controls.
+@interface BalloonShelfViewCocoa : NSView {
+}
+@end
+
+
+#endif // CHROME_BROWSER_UI_COCOA_NOTIFICATIONS_BALLOON_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/notifications/balloon_view.mm b/chrome/browser/ui/cocoa/notifications/balloon_view.mm
new file mode 100644
index 0000000..e88331a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/notifications/balloon_view.mm
@@ -0,0 +1,84 @@
+// 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 "chrome/browser/ui/cocoa/notifications/balloon_view.h"
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/basictypes.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/notifications/balloon_controller.h"
+#import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h"
+
+namespace {
+
+const int kRoundedCornerSize = 6;
+
+} // namespace
+
+@implementation BalloonWindow
+- (id)initWithContentRect:(NSRect)contentRect
+ styleMask:(unsigned int)aStyle
+ backing:(NSBackingStoreType)bufferingType
+ defer:(BOOL)flag {
+ self = [super initWithContentRect:contentRect
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ if (self) {
+ [self setLevel:NSStatusWindowLevel];
+ [self setOpaque:NO];
+ [self setBackgroundColor:[NSColor clearColor]];
+ }
+ return self;
+}
+
+- (BOOL)canBecomeMainWindow {
+ return NO;
+}
+
+- (void)sendEvent:(NSEvent*)event {
+ // We do not want to bring chrome window to foreground when we click on close
+ // or option button. To do this, we have to intercept the event.
+ BalloonController* delegate =
+ static_cast<BalloonController*>([self delegate]);
+ if (![delegate handleEvent:event]) {
+ [super sendEvent:event];
+ }
+}
+@end
+
+@implementation BalloonShelfViewCocoa
+- (void)drawRect:(NSRect)rect {
+ NSBezierPath* path =
+ [NSBezierPath gtm_bezierPathWithRoundRect:[self bounds]
+ topLeftCornerRadius:kRoundedCornerSize
+ topRightCornerRadius:kRoundedCornerSize
+ bottomLeftCornerRadius:0.0
+ bottomRightCornerRadius:0.0];
+
+ [[NSColor colorWithCalibratedWhite:0.957 alpha:1.0] set];
+ [path fill];
+
+ [[NSColor colorWithCalibratedWhite:0.8 alpha:1.0] set];
+ NSPoint origin = [self bounds].origin;
+ [NSBezierPath strokeLineFromPoint:origin
+ toPoint:NSMakePoint(origin.x + NSWidth([self bounds]), origin.y)];
+}
+@end
+
+@implementation BalloonContentViewCocoa
+- (void)drawRect:(NSRect)rect {
+ rect = NSInsetRect([self bounds], 0.5, 0.5);
+ NSBezierPath* path =
+ [NSBezierPath gtm_bezierPathWithRoundRect:rect
+ topLeftCornerRadius:0.0
+ topRightCornerRadius:0.0
+ bottomLeftCornerRadius:kRoundedCornerSize
+ bottomRightCornerRadius:kRoundedCornerSize];
+ [[NSColor whiteColor] set];
+ [path setLineWidth:3];
+ [path stroke];
+}
+@end
diff --git a/chrome/browser/ui/cocoa/notifications/balloon_view_bridge.h b/chrome/browser/ui/cocoa/notifications/balloon_view_bridge.h
new file mode 100644
index 0000000..3dff871
--- /dev/null
+++ b/chrome/browser/ui/cocoa/notifications/balloon_view_bridge.h
@@ -0,0 +1,40 @@
+// 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_UI_COCOA_NOTIFICATIONS_BALLOON_VIEW_BRIDGE_H_
+#define CHROME_BROWSER_UI_COCOA_NOTIFICATIONS_BALLOON_VIEW_BRIDGE_H_
+#pragma once
+
+#include "chrome/browser/notifications/balloon.h"
+
+@class BalloonController;
+class BalloonHost;
+namespace gfx {
+class Size;
+}
+
+// Bridges from the cross-platform BalloonView interface to the Cocoa
+// controller which will draw the view on screen.
+class BalloonViewBridge : public BalloonView {
+ public:
+ BalloonViewBridge();
+ ~BalloonViewBridge();
+
+ // BalloonView interface.
+ virtual void Show(Balloon* balloon);
+ virtual void Update();
+ virtual void RepositionToBalloon();
+ virtual void Close(bool by_user);
+ virtual gfx::Size GetSize() const;
+ virtual BalloonHost* GetHost() const;
+
+ private:
+ // Weak pointer to the balloon controller which manages the UI.
+ // This object cleans itself up when its windows close.
+ BalloonController* controller_;
+
+ DISALLOW_COPY_AND_ASSIGN(BalloonViewBridge);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_NOTIFICATIONS_BALLOON_VIEW_BRIDGE_H_
diff --git a/chrome/browser/ui/cocoa/notifications/balloon_view_bridge.mm b/chrome/browser/ui/cocoa/notifications/balloon_view_bridge.mm
new file mode 100644
index 0000000..cea53b1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/notifications/balloon_view_bridge.mm
@@ -0,0 +1,48 @@
+// 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 "chrome/browser/ui/cocoa/notifications/balloon_view_bridge.h"
+
+#include "chrome/browser/ui/cocoa/notifications/balloon_controller.h"
+#import "chrome/browser/ui/cocoa/notifications/balloon_view_host_mac.h"
+#include "gfx/size.h"
+
+#import <Cocoa/Cocoa.h>
+
+BalloonViewBridge::BalloonViewBridge() :
+ controller_(NULL) {
+}
+
+BalloonViewBridge::~BalloonViewBridge() {
+}
+
+void BalloonViewBridge::Close(bool by_user) {
+ [controller_ closeBalloon:by_user];
+}
+
+gfx::Size BalloonViewBridge::GetSize() const {
+ if (controller_)
+ return gfx::Size([controller_ desiredTotalWidth],
+ [controller_ desiredTotalHeight]);
+ else
+ return gfx::Size();
+}
+
+void BalloonViewBridge::RepositionToBalloon() {
+ [controller_ repositionToBalloon];
+}
+
+void BalloonViewBridge::Show(Balloon* balloon) {
+ controller_ = [[BalloonController alloc] initWithBalloon:balloon];
+ [controller_ setShouldCascadeWindows:NO];
+ [controller_ showWindow:nil];
+}
+
+BalloonHost* BalloonViewBridge::GetHost() const {
+ return [controller_ getHost];
+}
+
+void BalloonViewBridge::Update() {
+ [controller_ updateContents];
+}
diff --git a/chrome/browser/ui/cocoa/notifications/balloon_view_host_mac.h b/chrome/browser/ui/cocoa/notifications/balloon_view_host_mac.h
new file mode 100644
index 0000000..a7bff8f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/notifications/balloon_view_host_mac.h
@@ -0,0 +1,42 @@
+// 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_UI_COCOA_NOTIFICATIONS_BALLOON_VIEW_HOST_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_NOTIFICATIONS_BALLOON_VIEW_HOST_MAC_H_
+#pragma once
+
+#include "chrome/browser/notifications/balloon_host.h"
+
+class RenderWidgetHostView;
+class RenderWidgetHostViewMac;
+
+// BalloonViewHost class is a delegate to the renderer host for the HTML
+// notification. When initialized it creates a new RenderViewHost and loads
+// the contents of the toast into it. It also handles links within the toast,
+// loading them into a new tab.
+class BalloonViewHost : public BalloonHost {
+ public:
+ explicit BalloonViewHost(Balloon* balloon);
+
+ ~BalloonViewHost();
+
+ // Changes the size of the balloon.
+ void UpdateActualSize(const gfx::Size& new_size);
+
+ // Accessors.
+ gfx::NativeView native_view() const;
+
+ protected:
+ virtual void InitRenderWidgetHostView();
+ virtual RenderWidgetHostView* render_widget_host_view() const;
+
+ private:
+ // The Mac-specific widget host view. This is owned by its native view,
+ // which this class frees in its destructor.
+ RenderWidgetHostViewMac* render_widget_host_view_;
+
+ DISALLOW_COPY_AND_ASSIGN(BalloonViewHost);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_NOTIFICATIONS_BALLOON_VIEW_HOST_MAC_H_
diff --git a/chrome/browser/ui/cocoa/notifications/balloon_view_host_mac.mm b/chrome/browser/ui/cocoa/notifications/balloon_view_host_mac.mm
new file mode 100644
index 0000000..1f2c916
--- /dev/null
+++ b/chrome/browser/ui/cocoa/notifications/balloon_view_host_mac.mm
@@ -0,0 +1,39 @@
+// 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 "chrome/browser/ui/cocoa/notifications/balloon_view_host_mac.h"
+
+#include "chrome/browser/renderer_host/render_view_host.h"
+#include "chrome/browser/renderer_host/render_widget_host_view_mac.h"
+
+BalloonViewHost::BalloonViewHost(Balloon* balloon)
+ : BalloonHost(balloon) {
+}
+
+BalloonViewHost::~BalloonViewHost() {
+ Shutdown();
+}
+
+void BalloonViewHost::UpdateActualSize(const gfx::Size& new_size) {
+ NSView* view = render_widget_host_view_->native_view();
+ NSRect frame = [view frame];
+ frame.size.width = new_size.width();
+ frame.size.height = new_size.height();
+
+ [view setFrame:frame];
+ [view setNeedsDisplay:YES];
+}
+
+gfx::NativeView BalloonViewHost::native_view() const {
+ return render_widget_host_view_->native_view();
+}
+
+void BalloonViewHost::InitRenderWidgetHostView() {
+ DCHECK(render_view_host_);
+ render_widget_host_view_ = new RenderWidgetHostViewMac(render_view_host_);
+}
+
+RenderWidgetHostView* BalloonViewHost::render_widget_host_view() const {
+ return render_widget_host_view_;
+}
diff --git a/chrome/browser/ui/cocoa/nsimage_cache_unittest.mm b/chrome/browser/ui/cocoa/nsimage_cache_unittest.mm
new file mode 100644
index 0000000..7f2e2fb
--- /dev/null
+++ b/chrome/browser/ui/cocoa/nsimage_cache_unittest.mm
@@ -0,0 +1,75 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/file_path.h"
+#include "base/mac_util.h"
+#include "base/nsimage_cache_mac.h"
+#include "base/path_service.h"
+#include "chrome/common/chrome_constants.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+// This tests nsimage_cache, which lives in base/. The unit test is in
+// chrome/ because it depends on having a built-up Chrome present.
+
+namespace {
+
+class NSImageCacheTest : public PlatformTest {
+ public:
+ NSImageCacheTest() {
+ // Look in the framework bundle for resources.
+ FilePath path;
+ PathService::Get(base::DIR_EXE, &path);
+ path = path.Append(chrome::kFrameworkName);
+ mac_util::SetOverrideAppBundlePath(path);
+ }
+ virtual ~NSImageCacheTest() {
+ mac_util::SetOverrideAppBundle(nil);
+ }
+};
+
+TEST_F(NSImageCacheTest, LookupFound) {
+ EXPECT_TRUE(nsimage_cache::ImageNamed(@"back_Template.pdf") != nil)
+ << "Failed to find the toolbar image?";
+}
+
+TEST_F(NSImageCacheTest, LookupCached) {
+ EXPECT_EQ(nsimage_cache::ImageNamed(@"back_Template.pdf"),
+ nsimage_cache::ImageNamed(@"back_Template.pdf"))
+ << "Didn't get the same NSImage back?";
+}
+
+TEST_F(NSImageCacheTest, LookupMiss) {
+ EXPECT_TRUE(nsimage_cache::ImageNamed(@"should_not.exist") == nil)
+ << "There shouldn't be an image with this name?";
+}
+
+TEST_F(NSImageCacheTest, LookupFoundAndClear) {
+ NSImage *first = nsimage_cache::ImageNamed(@"back_Template.pdf");
+ // Hang on to the first image so that the second one doesn't get allocated
+ // in the same location by (bad) luck.
+ [[first retain] autorelease];
+ EXPECT_TRUE(first != nil)
+ << "Failed to find the toolbar image?";
+ nsimage_cache::Clear();
+ NSImage *second = nsimage_cache::ImageNamed(@"back_Template.pdf");
+ EXPECT_TRUE(second != nil)
+ << "Failed to find the toolbar image...again?";
+ EXPECT_NE(second, first)
+ << "how'd we get the same image after a cache clear?";
+}
+
+TEST_F(NSImageCacheTest, AutoTemplating) {
+ NSImage *templateImage = nsimage_cache::ImageNamed(@"back_Template.pdf");
+ EXPECT_TRUE([templateImage isTemplate] == YES)
+ << "Image ending in 'Template' should be marked as being a template";
+ NSImage *nonTemplateImage = nsimage_cache::ImageNamed(@"aliasCursor.png");
+ EXPECT_FALSE([nonTemplateImage isTemplate] == YES)
+ << "Image not ending in 'Template' should not be marked as being a "
+ "template";
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/nsmenuitem_additions.h b/chrome/browser/ui/cocoa/nsmenuitem_additions.h
new file mode 100644
index 0000000..830498a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/nsmenuitem_additions.h
@@ -0,0 +1,19 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_NSMENUITEM_ADDITIONS_H_
+#define CHROME_BROWSER_UI_COCOA_NSMENUITEM_ADDITIONS_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+@interface NSMenuItem(ChromeAdditions)
+
+// Returns true exactly if the menu item would fire if it would be put into
+// a menu and then |menu performKeyEquivalent:event| was called.
+- (BOOL)cr_firesForKeyEvent:(NSEvent*)event;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_NSMENUITEM_ADDITIONS_H_
diff --git a/chrome/browser/ui/cocoa/nsmenuitem_additions.mm b/chrome/browser/ui/cocoa/nsmenuitem_additions.mm
new file mode 100644
index 0000000..90bb353
--- /dev/null
+++ b/chrome/browser/ui/cocoa/nsmenuitem_additions.mm
@@ -0,0 +1,103 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/nsmenuitem_additions.h"
+
+#include <Carbon/Carbon.h>
+
+#include "base/logging.h"
+
+@implementation NSMenuItem(ChromeAdditions)
+
+- (BOOL)cr_firesForKeyEvent:(NSEvent*)event {
+ DCHECK([event type] == NSKeyDown);
+ if (![self isEnabled])
+ return NO;
+
+ // In System Preferences->Keyboard->Keyboard Shortcuts, it is possible to add
+ // arbitrary keyboard shortcuts to applications. It is not documented how this
+ // works in detail, but |NSMenuItem| has a method |userKeyEquivalent| that
+ // sounds related.
+ // However, it looks like |userKeyEquivalent| is equal to |keyEquivalent| when
+ // a user shortcut is set in system preferences, i.e. Cocoa automatically
+ // sets/overwrites |keyEquivalent| as well. Hence, this method can ignore
+ // |userKeyEquivalent| and check |keyEquivalent| only.
+
+ // Menu item key equivalents are nearly all stored without modifiers. The
+ // exception is shift, which is included in the key and not in the modifiers
+ // for printable characters (but not for stuff like arrow keys etc).
+ NSString* eventString = [event charactersIgnoringModifiers];
+ NSUInteger eventModifiers =
+ [event modifierFlags] & NSDeviceIndependentModifierFlagsMask;
+
+ if ([eventString length] == 0 || [[self keyEquivalent] length] == 0)
+ return NO;
+
+ // Turns out esc never fires unless cmd or ctrl is down.
+ if ([event keyCode] == kVK_Escape &&
+ (eventModifiers & (NSControlKeyMask | NSCommandKeyMask)) == 0)
+ return NO;
+
+ // From the |NSMenuItem setKeyEquivalent:| documentation:
+ //
+ // If you want to specify the Backspace key as the key equivalent for a menu
+ // item, use a single character string with NSBackspaceCharacter (defined in
+ // NSText.h as 0x08) and for the Forward Delete key, use NSDeleteCharacter
+ // (defined in NSText.h as 0x7F). Note that these are not the same characters
+ // you get from an NSEvent key-down event when pressing those keys.
+ if ([[self keyEquivalent] characterAtIndex:0] == NSBackspaceCharacter
+ && [eventString characterAtIndex:0] == NSDeleteCharacter) {
+ unichar chr = NSBackspaceCharacter;
+ eventString = [NSString stringWithCharacters:&chr length:1];
+
+ // Make sure "shift" is not removed from modifiers below.
+ eventModifiers |= NSFunctionKeyMask;
+ }
+ if ([[self keyEquivalent] characterAtIndex:0] == NSDeleteCharacter &&
+ [eventString characterAtIndex:0] == NSDeleteFunctionKey) {
+ unichar chr = NSDeleteCharacter;
+ eventString = [NSString stringWithCharacters:&chr length:1];
+
+ // Make sure "shift" is not removed from modifiers below.
+ eventModifiers |= NSFunctionKeyMask;
+ }
+
+ // cmd-opt-a gives some weird char as characters and "a" as
+ // charactersWithoutModifiers with an US layout, but an "a" as characters and
+ // a weird char as "charactersWithoutModifiers" with a cyrillic layout. Oh,
+ // Cocoa! Instead of getting the current layout from Text Input Services,
+ // and then requesting the kTISPropertyUnicodeKeyLayoutData and looking in
+ // there, let's try a pragmatic hack.
+ if ([eventString characterAtIndex:0] > 0x7f &&
+ [[event characters] length] > 0 &&
+ [[event characters] characterAtIndex:0] <= 0x7f)
+ eventString = [event characters];
+
+ // When both |characters| and |charactersIgnoringModifiers| are ascii, we
+ // want to use |characters| if it's a character and
+ // |charactersIgnoringModifiers| else (on dvorak, cmd-shift-z should fire
+ // "cmd-:" instead of "cmd-;", but on dvorak-qwerty, cmd-shift-z should fire
+ // cmd-shift-z instead of cmd-:).
+ if ([eventString characterAtIndex:0] <= 0x7f &&
+ [[event characters] length] > 0 &&
+ [[event characters] characterAtIndex:0] <= 0x7f &&
+ isalpha([[event characters] characterAtIndex:0]))
+ eventString = [event characters];
+
+ // Clear shift key for printable characters.
+ if ((eventModifiers & (NSNumericPadKeyMask | NSFunctionKeyMask)) == 0 &&
+ [[self keyEquivalent] characterAtIndex:0] != '\r')
+ eventModifiers &= ~NSShiftKeyMask;
+
+ // Clear all non-interesting modifiers
+ eventModifiers &= NSCommandKeyMask |
+ NSControlKeyMask |
+ NSAlternateKeyMask |
+ NSShiftKeyMask;
+
+ return [eventString isEqualToString:[self keyEquivalent]]
+ && eventModifiers == [self keyEquivalentModifierMask];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/nsmenuitem_additions_unittest.mm b/chrome/browser/ui/cocoa/nsmenuitem_additions_unittest.mm
new file mode 100644
index 0000000..ecbf07d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/nsmenuitem_additions_unittest.mm
@@ -0,0 +1,351 @@
+// 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/ui/cocoa/nsmenuitem_additions.h"
+
+#include <Carbon/Carbon.h>
+
+#include <ostream>
+
+#include "base/scoped_nsobject.h"
+#include "base/sys_string_conversions.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+NSEvent* KeyEvent(const NSUInteger modifierFlags,
+ NSString* chars,
+ NSString* charsNoMods,
+ const NSUInteger keyCode) {
+ return [NSEvent keyEventWithType:NSKeyDown
+ location:NSZeroPoint
+ modifierFlags:modifierFlags
+ timestamp:0.0
+ windowNumber:0
+ context:nil
+ characters:chars
+ charactersIgnoringModifiers:charsNoMods
+ isARepeat:NO
+ keyCode:keyCode];
+}
+
+NSMenuItem* MenuItem(NSString* equiv, NSUInteger mask) {
+ NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:@""
+ action:NULL
+ keyEquivalent:@""] autorelease];
+ [item setKeyEquivalent:equiv];
+ [item setKeyEquivalentModifierMask:mask];
+ return item;
+}
+
+std::ostream& operator<<(std::ostream& out, NSObject* obj) {
+ return out << base::SysNSStringToUTF8([obj description]);
+}
+
+std::ostream& operator<<(std::ostream& out, NSMenuItem* item) {
+ return out << "NSMenuItem " << base::SysNSStringToUTF8([item keyEquivalent]);
+}
+
+void ExpectKeyFiresItemEq(bool result, NSEvent* key, NSMenuItem* item,
+ bool compareCocoa) {
+ EXPECT_EQ(result, [item cr_firesForKeyEvent:key]) << key << '\n' << item;
+
+ // Make sure that Cocoa does in fact agree with our expectations. However,
+ // in some cases cocoa behaves weirdly (if you create e.g. a new event that
+ // contains all fields of the event that you get when hitting cmd-a with a
+ // russion keyboard layout, the copy won't fire a menu item that has cmd-a as
+ // key equivalent, even though the original event would) and isn't a good
+ // oracle function.
+ if (compareCocoa) {
+ scoped_nsobject<NSMenu> menu([[NSMenu alloc] initWithTitle:@"Menu!"]);
+ [menu setAutoenablesItems:NO];
+ EXPECT_FALSE([menu performKeyEquivalent:key]);
+ [menu addItem:item];
+ EXPECT_EQ(result, [menu performKeyEquivalent:key]) << key << '\n' << item;
+ }
+}
+
+void ExpectKeyFiresItem(
+ NSEvent* key, NSMenuItem* item, bool compareCocoa = true) {
+ ExpectKeyFiresItemEq(true, key, item, compareCocoa);
+}
+
+void ExpectKeyDoesntFireItem(
+ NSEvent* key, NSMenuItem* item, bool compareCocoa = true) {
+ ExpectKeyFiresItemEq(false, key, item, compareCocoa);
+}
+
+TEST(NSMenuItemAdditionsTest, TestFiresForKeyEvent) {
+ // These test cases were built by writing a small test app that has a
+ // MainMenu.xib with a given key equivalent set in Interface Builder and a
+ // some code that prints both the key equivalent that fires a menu item and
+ // the menu item's key equivalent and modifier masks. I then pasted those
+ // below. This was done with a US layout, unless otherwise noted. In the
+ // comments, "z" always means the physical "z" key on a US layout no matter
+ // what character that key produces.
+
+ NSMenuItem* item;
+ NSEvent* key;
+ unichar ch;
+ NSString* s;
+
+ // Sanity
+ item = MenuItem(@"", 0);
+ EXPECT_TRUE([item isEnabled]);
+
+ // a
+ key = KeyEvent(0x100, @"a", @"a", 0);
+ item = MenuItem(@"a", 0);
+ ExpectKeyFiresItem(key, item);
+ ExpectKeyDoesntFireItem(KeyEvent(0x20102, @"A", @"A", 0), item);
+
+ // Disabled menu item
+ key = KeyEvent(0x100, @"a", @"a", 0);
+ item = MenuItem(@"a", 0);
+ [item setEnabled:NO];
+ ExpectKeyDoesntFireItem(key, item, false);
+
+ // shift-a
+ key = KeyEvent(0x20102, @"A", @"A", 0);
+ item = MenuItem(@"A", 0);
+ ExpectKeyFiresItem(key, item);
+ ExpectKeyDoesntFireItem(KeyEvent(0x100, @"a", @"a", 0), item);
+
+ // cmd-opt-shift-a
+ key = KeyEvent(0x1a012a, @"\u00c5", @"A", 0);
+ item = MenuItem(@"A", 0x180000);
+ ExpectKeyFiresItem(key, item);
+
+ // cmd-opt-a
+ key = KeyEvent(0x18012a, @"\u00e5", @"a", 0);
+ item = MenuItem(@"a", 0x180000);
+ ExpectKeyFiresItem(key, item);
+
+ // cmd-=
+ key = KeyEvent(0x100110, @"=", @"=", 0x18);
+ item = MenuItem(@"=", 0x100000);
+ ExpectKeyFiresItem(key, item);
+
+ // cmd-shift-=
+ key = KeyEvent(0x12010a, @"=", @"+", 0x18);
+ item = MenuItem(@"+", 0x100000);
+ ExpectKeyFiresItem(key, item);
+
+ // Turns out Cocoa fires "+ 100108 + 18" if you hit cmd-= and the menu only
+ // has a cmd-+ shortcut. But that's transparent for |cr_firesForKeyEvent:|.
+
+ // ctrl-3
+ key = KeyEvent(0x40101, @"3", @"3", 0x14);
+ item = MenuItem(@"3", 0x40000);
+ ExpectKeyFiresItem(key, item);
+
+ // return
+ key = KeyEvent(0, @"\r", @"\r", 0x24);
+ item = MenuItem(@"\r", 0);
+ ExpectKeyFiresItem(key, item);
+
+ // shift-return
+ key = KeyEvent(0x20102, @"\r", @"\r", 0x24);
+ item = MenuItem(@"\r", 0x20000);
+ ExpectKeyFiresItem(key, item);
+
+ // shift-left
+ ch = NSLeftArrowFunctionKey;
+ s = [NSString stringWithCharacters:&ch length:1];
+ key = KeyEvent(0xa20102, s, s, 0x7b);
+ item = MenuItem(s, 0x20000);
+ ExpectKeyFiresItem(key, item);
+
+ // shift-f1 (with a layout that needs the fn key down for f1)
+ ch = NSF1FunctionKey;
+ s = [NSString stringWithCharacters:&ch length:1];
+ key = KeyEvent(0x820102, s, s, 0x7a);
+ item = MenuItem(s, 0x20000);
+ ExpectKeyFiresItem(key, item);
+
+ // esc
+ // Turns out this doesn't fire.
+ key = KeyEvent(0x100, @"\e", @"\e", 0x35);
+ item = MenuItem(@"\e", 0);
+ ExpectKeyDoesntFireItem(key,item, false);
+
+ // shift-esc
+ // Turns out this doesn't fire.
+ key = KeyEvent(0x20102, @"\e", @"\e", 0x35);
+ item = MenuItem(@"\e", 0x20000);
+ ExpectKeyDoesntFireItem(key,item, false);
+
+ // cmd-esc
+ key = KeyEvent(0x100108, @"\e", @"\e", 0x35);
+ item = MenuItem(@"\e", 0x100000);
+ ExpectKeyFiresItem(key, item);
+
+ // ctrl-esc
+ key = KeyEvent(0x40101, @"\e", @"\e", 0x35);
+ item = MenuItem(@"\e", 0x40000);
+ ExpectKeyFiresItem(key, item);
+
+ // delete ("backspace")
+ key = KeyEvent(0x100, @"\x7f", @"\x7f", 0x33);
+ item = MenuItem(@"\x08", 0);
+ ExpectKeyFiresItem(key, item, false);
+
+ // shift-delete
+ key = KeyEvent(0x20102, @"\x7f", @"\x7f", 0x33);
+ item = MenuItem(@"\x08", 0x20000);
+ ExpectKeyFiresItem(key, item, false);
+
+ // forwarddelete (fn-delete / fn-backspace)
+ ch = NSDeleteFunctionKey;
+ s = [NSString stringWithCharacters:&ch length:1];
+ key = KeyEvent(0x800100, s, s, 0x75);
+ item = MenuItem(@"\x7f", 0);
+ ExpectKeyFiresItem(key, item, false);
+
+ // shift-forwarddelete (shift-fn-delete / shift-fn-backspace)
+ ch = NSDeleteFunctionKey;
+ s = [NSString stringWithCharacters:&ch length:1];
+ key = KeyEvent(0x820102, s, s, 0x75);
+ item = MenuItem(@"\x7f", 0x20000);
+ ExpectKeyFiresItem(key, item, false);
+
+ // fn-left
+ ch = NSHomeFunctionKey;
+ s = [NSString stringWithCharacters:&ch length:1];
+ key = KeyEvent(0x800100, s, s, 0x73);
+ item = MenuItem(s, 0);
+ ExpectKeyFiresItem(key, item);
+
+ // cmd-left
+ ch = NSLeftArrowFunctionKey;
+ s = [NSString stringWithCharacters:&ch length:1];
+ key = KeyEvent(0xb00108, s, s, 0x7b);
+ item = MenuItem(s, 0x100000);
+ ExpectKeyFiresItem(key, item);
+
+ // Hitting the "a" key with a russian keyboard layout -- does not fire
+ // a menu item that has "a" as key equiv.
+ key = KeyEvent(0x100, @"\u0444", @"\u0444", 0);
+ item = MenuItem(@"a", 0);
+ ExpectKeyDoesntFireItem(key,item);
+
+ // cmd-a on a russion layout -- fires for a menu item with cmd-a as key equiv.
+ key = KeyEvent(0x100108, @"a", @"\u0444", 0);
+ item = MenuItem(@"a", 0x100000);
+ ExpectKeyFiresItem(key, item, false);
+
+ // cmd-z on US layout
+ key = KeyEvent(0x100108, @"z", @"z", 6);
+ item = MenuItem(@"z", 0x100000);
+ ExpectKeyFiresItem(key, item);
+
+ // cmd-y on german layout (has same keycode as cmd-z on us layout, shouldn't
+ // fire).
+ key = KeyEvent(0x100108, @"y", @"y", 6);
+ item = MenuItem(@"z", 0x100000);
+ ExpectKeyDoesntFireItem(key,item);
+
+ // cmd-z on german layout
+ key = KeyEvent(0x100108, @"z", @"z", 0x10);
+ item = MenuItem(@"z", 0x100000);
+ ExpectKeyFiresItem(key, item);
+
+ // fn-return (== enter)
+ key = KeyEvent(0x800100, @"\x3", @"\x3", 0x4c);
+ item = MenuItem(@"\r", 0);
+ ExpectKeyDoesntFireItem(key,item);
+
+ // cmd-z on dvorak layout (so that the key produces ';')
+ key = KeyEvent(0x100108, @";", @";", 6);
+ ExpectKeyDoesntFireItem(key, MenuItem(@"z", 0x100000));
+ ExpectKeyFiresItem(key, MenuItem(@";", 0x100000));
+
+ // cmd-z on dvorak qwerty layout (so that the key produces ';', but 'z' if
+ // cmd is down)
+ key = KeyEvent(0x100108, @"z", @";", 6);
+ ExpectKeyFiresItem(key, MenuItem(@"z", 0x100000), false);
+ ExpectKeyDoesntFireItem(key, MenuItem(@";", 0x100000), false);
+
+ // cmd-shift-z on dvorak layout (so that we get a ':')
+ key = KeyEvent(0x12010a, @";", @":", 6);
+ ExpectKeyFiresItem(key, MenuItem(@":", 0x100000));
+ ExpectKeyDoesntFireItem(key, MenuItem(@";", 0x100000));
+
+ // cmd-s with a serbian layout (just "s" produces something that looks a lot
+ // like "c" in some fonts, but is actually \u0441. cmd-s activates a menu item
+ // with key equivalent "s", not "c")
+ key = KeyEvent(0x100108, @"s", @"\u0441", 1);
+ ExpectKeyFiresItem(key, MenuItem(@"s", 0x100000), false);
+ ExpectKeyDoesntFireItem(key, MenuItem(@"c", 0x100000));
+}
+
+NSString* keyCodeToCharacter(NSUInteger keyCode,
+ EventModifiers modifiers,
+ TISInputSourceRef layout) {
+ CFDataRef uchr = (CFDataRef)TISGetInputSourceProperty(
+ layout, kTISPropertyUnicodeKeyLayoutData);
+ UCKeyboardLayout* keyLayout = (UCKeyboardLayout*)CFDataGetBytePtr(uchr);
+
+ UInt32 deadKeyState = 0;
+ OSStatus err = noErr;
+ UniCharCount maxStringLength = 4, actualStringLength;
+ UniChar unicodeString[4];
+ err = UCKeyTranslate(keyLayout,
+ (UInt16)keyCode,
+ kUCKeyActionDown,
+ modifiers,
+ LMGetKbdType(),
+ kUCKeyTranslateNoDeadKeysBit,
+ &deadKeyState,
+ maxStringLength,
+ &actualStringLength,
+ unicodeString);
+ assert(err == noErr);
+
+ CFStringRef temp = CFStringCreateWithCharacters(
+ kCFAllocatorDefault, unicodeString, 1);
+ return [(NSString*)temp autorelease];
+}
+
+TEST(NSMenuItemAdditionsTest, TestMOnDifferentLayouts) {
+ // There's one key -- "m" -- that has the same keycode on most keyboard
+ // layouts. This function tests a menu item with cmd-m as key equivalent
+ // can be fired on all layouts.
+ NSMenuItem* item = MenuItem(@"m", 0x100000);
+
+ NSDictionary* filter = [NSDictionary
+ dictionaryWithObject:(NSString*)kTISTypeKeyboardLayout
+ forKey:(NSString*)kTISPropertyInputSourceType];
+
+ // Docs say that including all layouts instead of just the active ones is
+ // slow, but there's no way around that.
+ NSArray* list = (NSArray*)TISCreateInputSourceList(
+ (CFDictionaryRef)filter, true);
+ for (id layout in list) {
+ TISInputSourceRef ref = (TISInputSourceRef)layout;
+
+ NSUInteger keyCode = 0x2e; // "m" on a US layout and most other layouts.
+
+ // On a few layouts, "m" has a different key code.
+ NSString* layoutId = (NSString*)TISGetInputSourceProperty(
+ ref, kTISPropertyInputSourceID);
+ if ([layoutId isEqualToString:@"com.apple.keylayout.Belgian"] ||
+ [layoutId isEqualToString:@"com.apple.keylayout.French"] ||
+ [layoutId isEqualToString:@"com.apple.keylayout.French-numerical"] ||
+ [layoutId isEqualToString:@"com.apple.keylayout.Italian"]) {
+ keyCode = 0x29;
+ } else if ([layoutId isEqualToString:@"com.apple.keylayout.Turkish"]) {
+ keyCode = 0x28;
+ } else if ([layoutId isEqualToString:@"com.apple.keylayout.Dvorak-Left"]) {
+ keyCode = 0x16;
+ } else if ([layoutId isEqualToString:@"com.apple.keylayout.Dvorak-Right"]) {
+ keyCode = 0x1a;
+ }
+
+ EventModifiers modifiers = cmdKey >> 8;
+ NSString* chars = keyCodeToCharacter(keyCode, modifiers, ref);
+ NSString* charsIgnoringMods = keyCodeToCharacter(keyCode, 0, ref);
+ NSEvent* key = KeyEvent(0x100000, chars, charsIgnoringMods, keyCode);
+ ExpectKeyFiresItem(key, item, false);
+ }
+ CFRelease(list);
+}
diff --git a/chrome/browser/ui/cocoa/nswindow_additions.h b/chrome/browser/ui/cocoa/nswindow_additions.h
new file mode 100644
index 0000000..9d79464
--- /dev/null
+++ b/chrome/browser/ui/cocoa/nswindow_additions.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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_NSWINDOW_ADDITIONS_H_
+#define CHROME_BROWSER_UI_COCOA_NSWINDOW_ADDITIONS_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// ID of a Space. Starts at 1.
+typedef int CGSWorkspaceID;
+
+@interface NSWindow(ChromeAdditions)
+
+// Gets the Space that the window is currently on. YES on success, NO on
+// failure.
+- (BOOL)cr_workspace:(CGSWorkspaceID*)outWorkspace;
+
+// Moves the window to the given Space. YES on success, NO on failure.
+- (BOOL)cr_moveToWorkspace:(CGSWorkspaceID)workspace;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_NSWINDOW_ADDITIONS_H_
diff --git a/chrome/browser/ui/cocoa/nswindow_additions.mm b/chrome/browser/ui/cocoa/nswindow_additions.mm
new file mode 100644
index 0000000..f06af0d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/nswindow_additions.mm
@@ -0,0 +1,104 @@
+// 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/ui/cocoa/nswindow_additions.h"
+
+#include <dlfcn.h>
+
+#include "base/logging.h"
+
+typedef void* CGSConnectionID;
+typedef int CGSWindowID;
+typedef int CGSError;
+typedef int CGSWorkspaceID;
+
+// These are private APIs we look up at run time.
+typedef CGSConnectionID (*CGSDefaultConnectionFunc)(void);
+typedef CGSError (*CGSGetWindowWorkspaceFunc)(const CGSConnectionID cid,
+ CGSWindowID wid,
+ CGSWorkspaceID* workspace);
+typedef CGSError (*CGSMoveWorkspaceWindowListFunc)(const CGSConnectionID cid,
+ CGSWindowID* wids,
+ int count,
+ CGSWorkspaceID workspace);
+
+static CGSDefaultConnectionFunc sCGSDefaultConnection;
+static CGSGetWindowWorkspaceFunc sCGSGetWindowWorkspace;
+static CGSMoveWorkspaceWindowListFunc sCGSMoveWorkspaceWindowList;
+
+@implementation NSWindow(ChromeAdditions)
+
+// Looks up private Spaces APIs using dlsym.
+- (BOOL)cr_initializeWorkspaceAPIs {
+ static BOOL shouldInitialize = YES;
+ if (shouldInitialize) {
+ shouldInitialize = NO;
+
+ NSBundle* coreGraphicsBundle =
+ [NSBundle bundleWithIdentifier:@"com.apple.CoreGraphics"];
+ NSString* coreGraphicsPath = [[coreGraphicsBundle bundlePath]
+ stringByAppendingPathComponent:@"CoreGraphics"];
+ void* coreGraphicsLibrary = dlopen([coreGraphicsPath UTF8String],
+ RTLD_GLOBAL | RTLD_LAZY);
+
+ if (coreGraphicsLibrary) {
+ sCGSDefaultConnection =
+ (CGSDefaultConnectionFunc)dlsym(coreGraphicsLibrary,
+ "_CGSDefaultConnection");
+ if (!sCGSDefaultConnection) {
+ LOG(ERROR) << "Failed to lookup _CGSDefaultConnection API" << dlerror();
+ }
+ sCGSGetWindowWorkspace =
+ (CGSGetWindowWorkspaceFunc)dlsym(coreGraphicsLibrary,
+ "CGSGetWindowWorkspace");
+ if (!sCGSGetWindowWorkspace) {
+ LOG(ERROR) << "Failed to lookup CGSGetWindowWorkspace API" << dlerror();
+ }
+ sCGSMoveWorkspaceWindowList =
+ (CGSMoveWorkspaceWindowListFunc)dlsym(coreGraphicsLibrary,
+ "CGSMoveWorkspaceWindowList");
+ if (!sCGSMoveWorkspaceWindowList) {
+ LOG(ERROR) << "Failed to lookup CGSMoveWorkspaceWindowList API"
+ << dlerror();
+ }
+ } else {
+ LOG(ERROR) << "Failed to load CoreGraphics lib" << dlerror();
+ }
+ }
+
+ return sCGSDefaultConnection != NULL &&
+ sCGSGetWindowWorkspace != NULL &&
+ sCGSMoveWorkspaceWindowList != NULL;
+}
+
+- (BOOL)cr_workspace:(CGSWorkspaceID*)outWorkspace {
+ if (![self cr_initializeWorkspaceAPIs]) {
+ return NO;
+ }
+
+ // If this ASSERT fails then consider using CGSDefaultConnectionForThread()
+ // instead of CGSDefaultConnection().
+ DCHECK([NSThread isMainThread]);
+ CGSConnectionID cid = sCGSDefaultConnection();
+ CGSWindowID wid = [self windowNumber];
+ CGSError err = sCGSGetWindowWorkspace(cid, wid, outWorkspace);
+ return err == 0;
+}
+
+- (BOOL)cr_moveToWorkspace:(CGSWorkspaceID)workspace {
+ if (![self cr_initializeWorkspaceAPIs]) {
+ return NO;
+ }
+
+ // If this ASSERT fails then consider using CGSDefaultConnectionForThread()
+ // instead of CGSDefaultConnection().
+ DCHECK([NSThread isMainThread]);
+ CGSConnectionID cid = sCGSDefaultConnection();
+ CGSWindowID wid = [self windowNumber];
+ // CGSSetWorkspaceForWindow doesn't seem to work for some reason.
+ CGSError err = sCGSMoveWorkspaceWindowList(cid, &wid, 1, workspace);
+ return err == 0;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/objc_method_swizzle.h b/chrome/browser/ui/cocoa/objc_method_swizzle.h
new file mode 100644
index 0000000..2b94832
--- /dev/null
+++ b/chrome/browser/ui/cocoa/objc_method_swizzle.h
@@ -0,0 +1,28 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_OBJC_METHOD_SWIZZLE_H_
+#define CHROME_BROWSER_UI_COCOA_OBJC_METHOD_SWIZZLE_H_
+#pragma once
+
+#import <objc/objc-class.h>
+
+// You should think twice every single time you use anything from this
+// namespace.
+namespace ObjcEvilDoers {
+
+// This is similar to class_getInstanceMethod(), except that it
+// returns NULL if |aClass| does not directly implement |aSelector|.
+Method GetImplementedInstanceMethod(Class aClass, SEL aSelector);
+
+// Exchanges the implementation of |originalSelector| and
+// |alternateSelector| within |aClass|. Both selectors must be
+// implemented directly by |aClass|, not inherited. The IMP returned
+// is for |originalSelector| (for purposes of forwarding).
+IMP SwizzleImplementedInstanceMethods(
+ Class aClass, const SEL originalSelector, const SEL alternateSelector);
+
+} // namespace ObjcEvilDoers
+
+#endif // CHROME_BROWSER_UI_COCOA_OBJC_METHOD_SWIZZLE_H_
diff --git a/chrome/browser/ui/cocoa/objc_method_swizzle.mm b/chrome/browser/ui/cocoa/objc_method_swizzle.mm
new file mode 100644
index 0000000..34f88a5
--- /dev/null
+++ b/chrome/browser/ui/cocoa/objc_method_swizzle.mm
@@ -0,0 +1,59 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/objc_method_swizzle.h"
+
+#import "base/logging.h"
+#import "base/scoped_nsobject.h"
+#import "chrome/app/breakpad_mac.h"
+
+namespace ObjcEvilDoers {
+
+Method GetImplementedInstanceMethod(Class aClass, SEL aSelector) {
+ Method method = NULL;
+ unsigned int methodCount = 0;
+ Method* methodList = class_copyMethodList(aClass, &methodCount);
+ if (methodList) {
+ for (unsigned int i = 0; i < methodCount; ++i) {
+ if (method_getName(methodList[i]) == aSelector) {
+ method = methodList[i];
+ break;
+ }
+ }
+ free(methodList);
+ }
+ return method;
+}
+
+IMP SwizzleImplementedInstanceMethods(
+ Class aClass, const SEL originalSelector, const SEL alternateSelector) {
+ // The methods must both be implemented by the target class, not
+ // inherited from a superclass.
+ Method original = GetImplementedInstanceMethod(aClass, originalSelector);
+ Method alternate = GetImplementedInstanceMethod(aClass, alternateSelector);
+ DCHECK(original);
+ DCHECK(alternate);
+ if (!original || !alternate) {
+ return NULL;
+ }
+
+ // The argument and return types must match exactly.
+ const char* originalTypes = method_getTypeEncoding(original);
+ const char* alternateTypes = method_getTypeEncoding(alternate);
+ DCHECK(originalTypes);
+ DCHECK(alternateTypes);
+ DCHECK(0 == strcmp(originalTypes, alternateTypes));
+ if (!originalTypes || !alternateTypes ||
+ strcmp(originalTypes, alternateTypes)) {
+ return NULL;
+ }
+
+ IMP ret = method_getImplementation(original);
+ if (ret) {
+ method_exchangeImplementations(original, alternate);
+ }
+ return ret;
+}
+
+} // namespace ObjcEvilDoers
diff --git a/chrome/browser/ui/cocoa/objc_method_swizzle_unittest.mm b/chrome/browser/ui/cocoa/objc_method_swizzle_unittest.mm
new file mode 100644
index 0000000..1641741
--- /dev/null
+++ b/chrome/browser/ui/cocoa/objc_method_swizzle_unittest.mm
@@ -0,0 +1,76 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/objc_method_swizzle.h"
+
+#include "base/scoped_nsobject.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+@interface ObjcMethodSwizzleTest : NSObject
+- (id)self;
+
+- (NSInteger)one;
+- (NSInteger)two;
+@end
+
+@implementation ObjcMethodSwizzleTest : NSObject
+- (id)self {
+ return [super self];
+}
+
+- (NSInteger)one {
+ return 1;
+}
+- (NSInteger)two {
+ return 2;
+}
+@end
+
+@interface ObjcMethodSwizzleTest (ObjcMethodSwizzleTestCategory)
+- (NSUInteger)hash;
+@end
+
+@implementation ObjcMethodSwizzleTest (ObjcMethodSwizzleTestCategory)
+- (NSUInteger)hash {
+ return [super hash];
+}
+@end
+
+namespace ObjcEvilDoers {
+
+TEST(ObjcMethodSwizzleTest, GetImplementedInstanceMethod) {
+ EXPECT_EQ(class_getInstanceMethod([NSObject class], @selector(dealloc)),
+ GetImplementedInstanceMethod([NSObject class], @selector(dealloc)));
+ EXPECT_EQ(class_getInstanceMethod([NSObject class], @selector(self)),
+ GetImplementedInstanceMethod([NSObject class], @selector(self)));
+ EXPECT_EQ(class_getInstanceMethod([NSObject class], @selector(hash)),
+ GetImplementedInstanceMethod([NSObject class], @selector(hash)));
+
+ Class testClass = [ObjcMethodSwizzleTest class];
+ EXPECT_EQ(class_getInstanceMethod(testClass, @selector(self)),
+ GetImplementedInstanceMethod(testClass, @selector(self)));
+ EXPECT_NE(class_getInstanceMethod([NSObject class], @selector(self)),
+ class_getInstanceMethod(testClass, @selector(self)));
+
+ EXPECT_TRUE(class_getInstanceMethod(testClass, @selector(dealloc)));
+ EXPECT_FALSE(GetImplementedInstanceMethod(testClass, @selector(dealloc)));
+}
+
+TEST(ObjcMethodSwizzleTest, SwizzleImplementedInstanceMethods) {
+ scoped_nsobject<ObjcMethodSwizzleTest> object(
+ [[ObjcMethodSwizzleTest alloc] init]);
+ EXPECT_EQ([object one], 1);
+ EXPECT_EQ([object two], 2);
+
+ Class testClass = [object class];
+ SwizzleImplementedInstanceMethods(testClass, @selector(one), @selector(two));
+ EXPECT_EQ([object one], 2);
+ EXPECT_EQ([object two], 1);
+
+ SwizzleImplementedInstanceMethods(testClass, @selector(one), @selector(two));
+ EXPECT_EQ([object one], 1);
+ EXPECT_EQ([object two], 2);
+}
+
+} // namespace ObjcEvilDoers
diff --git a/chrome/browser/ui/cocoa/objc_zombie.h b/chrome/browser/ui/cocoa/objc_zombie.h
new file mode 100644
index 0000000..714409e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/objc_zombie.h
@@ -0,0 +1,39 @@
+// 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_UI_COCOA_NSOBJECT_ZOMBIE_H_
+#define CHROME_BROWSER_UI_COCOA_NSOBJECT_ZOMBIE_H_
+#pragma once
+
+#import <Foundation/Foundation.h>
+
+// You should think twice every single time you use anything from this
+// namespace.
+namespace ObjcEvilDoers {
+
+// Enable zombie object debugging. This implements a variant of Apple's
+// NSZombieEnabled which can help expose use-after-free errors where messages
+// are sent to freed Objective-C objects in production builds.
+//
+// Returns NO if it fails to enable.
+//
+// When |zombieAllObjects| is YES, all objects inheriting from
+// NSObject become zombies on -dealloc. If NO, -shouldBecomeCrZombie
+// is queried to determine whether to make the object a zombie.
+//
+// |zombieCount| controls how many zombies to store before freeing the
+// oldest. Set to 0 to free objects immediately after making them
+// zombies.
+BOOL ZombieEnable(BOOL zombieAllObjects, size_t zombieCount);
+
+// Disable zombies.
+void ZombieDisable();
+
+} // namespace ObjcEvilDoers
+
+@interface NSObject (CrZombie)
+- (BOOL)shouldBecomeCrZombie;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_NSOBJECT_ZOMBIE_H_
diff --git a/chrome/browser/ui/cocoa/objc_zombie.mm b/chrome/browser/ui/cocoa/objc_zombie.mm
new file mode 100644
index 0000000..6802fd2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/objc_zombie.mm
@@ -0,0 +1,414 @@
+// 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/ui/cocoa/objc_zombie.h"
+
+#include <dlfcn.h>
+#include <mach-o/dyld.h>
+#include <mach-o/nlist.h>
+
+#import <objc/objc-class.h>
+
+#include "base/lock.h"
+#include "base/logging.h"
+#import "chrome/app/breakpad_mac.h"
+#import "chrome/browser/ui/cocoa/objc_method_swizzle.h"
+
+// Deallocated objects are re-classed as |CrZombie|. No superclass
+// because then the class would have to override many/most of the
+// inherited methods (|NSObject| is like a category magnet!).
+@interface CrZombie {
+ Class isa;
+}
+@end
+
+// Objects with enough space are made into "fat" zombies, which
+// directly remember which class they were until reallocated.
+@interface CrFatZombie : CrZombie {
+ @public
+ Class wasa;
+}
+@end
+
+namespace {
+
+// |object_cxxDestruct()| is an Objective-C runtime function which
+// traverses the object's class tree for ".cxxdestruct" methods which
+// are run to call C++ destructors as part of |-dealloc|. The
+// function is not public, so must be looked up using nlist.
+typedef void DestructFn(id obj);
+DestructFn* g_object_cxxDestruct = NULL;
+
+// The original implementation for |-[NSObject dealloc]|.
+IMP g_originalDeallocIMP = NULL;
+
+// Classes which freed objects become. |g_fatZombieSize| is the
+// minimum object size which can be made into a fat zombie (which can
+// remember which class it was before free, even after falling off the
+// treadmill).
+Class g_zombieClass = Nil; // cached [CrZombie class]
+Class g_fatZombieClass = Nil; // cached [CrFatZombie class]
+size_t g_fatZombieSize = 0;
+
+// Whether to zombie all freed objects, or only those which return YES
+// from |-shouldBecomeCrZombie|.
+BOOL g_zombieAllObjects = NO;
+
+// Protects |g_zombieCount|, |g_zombieIndex|, and |g_zombies|.
+Lock lock_;
+
+// How many zombies to keep before freeing, and the current head of
+// the circular buffer.
+size_t g_zombieCount = 0;
+size_t g_zombieIndex = 0;
+
+typedef struct {
+ id object; // The zombied object.
+ Class wasa; // Value of |object->isa| before we replaced it.
+} ZombieRecord;
+
+ZombieRecord* g_zombies = NULL;
+
+// Lookup the private |object_cxxDestruct| function and return a
+// pointer to it. Returns |NULL| on failure.
+DestructFn* LookupObjectCxxDestruct() {
+#if ARCH_CPU_64_BITS
+ // TODO(shess): Port to 64-bit. I believe using struct nlist_64
+ // will suffice. http://crbug.com/44021 .
+ NOTIMPLEMENTED();
+ return NULL;
+#endif
+
+ struct nlist nl[3];
+ bzero(&nl, sizeof(nl));
+
+ nl[0].n_un.n_name = (char*)"_object_cxxDestruct";
+
+ // My ability to calculate the base for offsets is apparently poor.
+ // Use |class_addIvar| as a known reference point.
+ nl[1].n_un.n_name = (char*)"_class_addIvar";
+
+ if (nlist("/usr/lib/libobjc.dylib", nl) < 0 ||
+ nl[0].n_type == N_UNDF || nl[1].n_type == N_UNDF)
+ return NULL;
+
+ return (DestructFn*)((char*)&class_addIvar - nl[1].n_value + nl[0].n_value);
+}
+
+// Replacement |-dealloc| which turns objects into zombies and places
+// them into |g_zombies| to be freed later.
+void ZombieDealloc(id self, SEL _cmd) {
+ // This code should only be called when it is implementing |-dealloc|.
+ DCHECK_EQ(_cmd, @selector(dealloc));
+
+ // Use the original |-dealloc| if the object doesn't wish to be
+ // zombied.
+ if (!g_zombieAllObjects && ![self shouldBecomeCrZombie]) {
+ g_originalDeallocIMP(self, _cmd);
+ return;
+ }
+
+ // Use the original |-dealloc| if |object_cxxDestruct| was never
+ // initialized, because otherwise C++ destructors won't be called.
+ // This case should be impossible, but doing it wrong would cause
+ // terrible problems.
+ DCHECK(g_object_cxxDestruct);
+ if (!g_object_cxxDestruct) {
+ g_originalDeallocIMP(self, _cmd);
+ return;
+ }
+
+ Class wasa = object_getClass(self);
+ const size_t size = class_getInstanceSize(wasa);
+
+ // Destroy the instance by calling C++ destructors and clearing it
+ // to something unlikely to work well if someone references it.
+ (*g_object_cxxDestruct)(self);
+ memset(self, '!', size);
+
+ // If the instance is big enough, make it into a fat zombie and have
+ // it remember the old |isa|. Otherwise make it a regular zombie.
+ // Setting |isa| rather than using |object_setClass()| because that
+ // function is implemented with a memory barrier. The runtime's
+ // |_internal_object_dispose()| (in objc-class.m) does this, so it
+ // should be safe (messaging free'd objects shouldn't be expected to
+ // be thread-safe in the first place).
+ if (size >= g_fatZombieSize) {
+ self->isa = g_fatZombieClass;
+ static_cast<CrFatZombie*>(self)->wasa = wasa;
+ } else {
+ self->isa = g_zombieClass;
+ }
+
+ // The new record to swap into |g_zombies|. If |g_zombieCount| is
+ // zero, then |self| will be freed immediately.
+ ZombieRecord zombieToFree = {self, wasa};
+
+ // Don't involve the lock when creating zombies without a treadmill.
+ if (g_zombieCount > 0) {
+ AutoLock pin(lock_);
+
+ // Check the count again in a thread-safe manner.
+ if (g_zombieCount > 0) {
+ // Put the current object on the treadmill and keep the previous
+ // occupant.
+ std::swap(zombieToFree, g_zombies[g_zombieIndex]);
+
+ // Bump the index forward.
+ g_zombieIndex = (g_zombieIndex + 1) % g_zombieCount;
+ }
+ }
+
+ // Do the free out here to prevent any chance of deadlock.
+ if (zombieToFree.object)
+ free(zombieToFree.object);
+}
+
+// Attempt to determine the original class of zombie |object|.
+Class ZombieWasa(id object) {
+ // Fat zombies can hold onto their |wasa| past the point where the
+ // object was actually freed. Note that to arrive here at all,
+ // |object|'s memory must still be accessible.
+ if (object_getClass(object) == g_fatZombieClass)
+ return static_cast<CrFatZombie*>(object)->wasa;
+
+ // For instances which weren't big enough to store |wasa|, check if
+ // the object is still on the treadmill.
+ AutoLock pin(lock_);
+ for (size_t i=0; i < g_zombieCount; ++i) {
+ if (g_zombies[i].object == object)
+ return g_zombies[i].wasa;
+ }
+
+ return Nil;
+}
+
+// Log a message to a freed object. |wasa| is the object's original
+// class. |aSelector| is the selector which the calling code was
+// attempting to send. |viaSelector| is the selector of the
+// dispatch-related method which is being invoked to send |aSelector|
+// (for instance, -respondsToSelector:).
+void ZombieObjectCrash(id object, SEL aSelector, SEL viaSelector) {
+ Class wasa = ZombieWasa(object);
+ const char* wasaName = (wasa ? class_getName(wasa) : "<unknown>");
+ NSString* aString =
+ [NSString stringWithFormat:@"Zombie <%s: %p> received -%s",
+ wasaName, object, sel_getName(aSelector)];
+ if (viaSelector != NULL) {
+ const char* viaName = sel_getName(viaSelector);
+ aString = [aString stringByAppendingFormat:@" (via -%s)", viaName];
+ }
+
+ // Set a value for breakpad to report, then crash.
+ SetCrashKeyValue(@"zombie", aString);
+ LOG(ERROR) << [aString UTF8String];
+
+ // This is how about:crash is implemented. Using instead of
+ // |DebugUtil::BreakDebugger()| or |LOG(FATAL)| to make the top of
+ // stack more immediately obvious in crash dumps.
+ int* zero = NULL;
+ *zero = 0;
+}
+
+// Initialize our globals, returning YES on success.
+BOOL ZombieInit() {
+ static BOOL initialized = NO;
+ if (initialized)
+ return YES;
+
+ Class rootClass = [NSObject class];
+
+ g_object_cxxDestruct = LookupObjectCxxDestruct();
+ g_originalDeallocIMP =
+ class_getMethodImplementation(rootClass, @selector(dealloc));
+ // objc_getClass() so CrZombie doesn't need +class.
+ g_zombieClass = objc_getClass("CrZombie");
+ g_fatZombieClass = objc_getClass("CrFatZombie");
+ g_fatZombieSize = class_getInstanceSize(g_fatZombieClass);
+
+ if (!g_object_cxxDestruct || !g_originalDeallocIMP ||
+ !g_zombieClass || !g_fatZombieClass)
+ return NO;
+
+ initialized = YES;
+ return YES;
+}
+
+} // namespace
+
+@implementation CrZombie
+
+// The Objective-C runtime needs to be able to call this successfully.
++ (void)initialize {
+}
+
+// Any method not explicitly defined will end up here, forcing a
+// crash.
+- (id)forwardingTargetForSelector:(SEL)aSelector {
+ ZombieObjectCrash(self, aSelector, NULL);
+ return nil;
+}
+
+// Override a few methods often used for dynamic dispatch to log the
+// message the caller is attempting to send, rather than the utility
+// method being used to send it.
+- (BOOL)respondsToSelector:(SEL)aSelector {
+ ZombieObjectCrash(self, aSelector, _cmd);
+ return NO;
+}
+
+- (id)performSelector:(SEL)aSelector {
+ ZombieObjectCrash(self, aSelector, _cmd);
+ return nil;
+}
+
+- (id)performSelector:(SEL)aSelector withObject:(id)anObject {
+ ZombieObjectCrash(self, aSelector, _cmd);
+ return nil;
+}
+
+- (id)performSelector:(SEL)aSelector
+ withObject:(id)anObject
+ withObject:(id)anotherObject {
+ ZombieObjectCrash(self, aSelector, _cmd);
+ return nil;
+}
+
+- (void)performSelector:(SEL)aSelector
+ withObject:(id)anArgument
+ afterDelay:(NSTimeInterval)delay {
+ ZombieObjectCrash(self, aSelector, _cmd);
+}
+
+@end
+
+@implementation CrFatZombie
+
+// This implementation intentionally left empty.
+
+@end
+
+@implementation NSObject (CrZombie)
+
+- (BOOL)shouldBecomeCrZombie {
+ return NO;
+}
+
+@end
+
+namespace ObjcEvilDoers {
+
+BOOL ZombieEnable(BOOL zombieAllObjects,
+ size_t zombieCount) {
+ // Only allow enable/disable on the main thread, just to keep things
+ // simple.
+ CHECK([NSThread isMainThread]);
+
+ if (!ZombieInit())
+ return NO;
+
+ g_zombieAllObjects = zombieAllObjects;
+
+ // Replace the implementation of -[NSObject dealloc].
+ Method m = class_getInstanceMethod([NSObject class], @selector(dealloc));
+ if (!m)
+ return NO;
+
+ const IMP prevDeallocIMP = method_setImplementation(m, (IMP)ZombieDealloc);
+ DCHECK(prevDeallocIMP == g_originalDeallocIMP ||
+ prevDeallocIMP == (IMP)ZombieDealloc);
+
+ // Grab the current set of zombies. This is thread-safe because
+ // only the main thread can change these.
+ const size_t oldCount = g_zombieCount;
+ ZombieRecord* oldZombies = g_zombies;
+
+ {
+ AutoLock pin(lock_);
+
+ // Save the old index in case zombies need to be transferred.
+ size_t oldIndex = g_zombieIndex;
+
+ // Create the new zombie treadmill, disabling zombies in case of
+ // failure.
+ g_zombieIndex = 0;
+ g_zombieCount = zombieCount;
+ g_zombies = NULL;
+ if (g_zombieCount) {
+ g_zombies =
+ static_cast<ZombieRecord*>(calloc(g_zombieCount, sizeof(*g_zombies)));
+ if (!g_zombies) {
+ NOTREACHED();
+ g_zombies = oldZombies;
+ g_zombieCount = oldCount;
+ g_zombieIndex = oldIndex;
+ ZombieDisable();
+ return NO;
+ }
+ }
+
+ // If the count is changing, allow some of the zombies to continue
+ // shambling forward.
+ const size_t sharedCount = std::min(oldCount, zombieCount);
+ if (sharedCount) {
+ // Get index of the first shared zombie.
+ oldIndex = (oldIndex + oldCount - sharedCount) % oldCount;
+
+ for (; g_zombieIndex < sharedCount; ++ g_zombieIndex) {
+ DCHECK_LT(g_zombieIndex, g_zombieCount);
+ DCHECK_LT(oldIndex, oldCount);
+ std::swap(g_zombies[g_zombieIndex], oldZombies[oldIndex]);
+ oldIndex = (oldIndex + 1) % oldCount;
+ }
+ g_zombieIndex %= g_zombieCount;
+ }
+ }
+
+ // Free the old treadmill and any remaining zombies.
+ if (oldZombies) {
+ for (size_t i = 0; i < oldCount; ++i) {
+ if (oldZombies[i].object)
+ free(oldZombies[i].object);
+ }
+ free(oldZombies);
+ }
+
+ return YES;
+}
+
+void ZombieDisable() {
+ // Only allow enable/disable on the main thread, just to keep things
+ // simple.
+ CHECK([NSThread isMainThread]);
+
+ // |ZombieInit()| was never called.
+ if (!g_originalDeallocIMP)
+ return;
+
+ // Put back the original implementation of -[NSObject dealloc].
+ Method m = class_getInstanceMethod([NSObject class], @selector(dealloc));
+ CHECK(m);
+ method_setImplementation(m, g_originalDeallocIMP);
+
+ // Can safely grab this because it only happens on the main thread.
+ const size_t oldCount = g_zombieCount;
+ ZombieRecord* oldZombies = g_zombies;
+
+ {
+ AutoLock pin(lock_); // In case any |-dealloc| are in-progress.
+ g_zombieCount = 0;
+ g_zombies = NULL;
+ }
+
+ // Free any remaining zombies.
+ if (oldZombies) {
+ for (size_t i = 0; i < oldCount; ++i) {
+ if (oldZombies[i].object)
+ free(oldZombies[i].object);
+ }
+ free(oldZombies);
+ }
+}
+
+} // namespace ObjcEvilDoers
diff --git a/chrome/browser/ui/cocoa/page_info_bubble_controller.h b/chrome/browser/ui/cocoa/page_info_bubble_controller.h
new file mode 100644
index 0000000..10909c6
--- /dev/null
+++ b/chrome/browser/ui/cocoa/page_info_bubble_controller.h
@@ -0,0 +1,47 @@
+// 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"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/page_info_model.h"
+#import "chrome/browser/ui/cocoa/base_bubble_controller.h"
+
+// This NSWindowController subclass manages the InfoBubbleWindow and view that
+// are displayed when the user clicks the security lock icon.
+@interface PageInfoBubbleController : BaseBubbleController {
+ @private
+ // The model that generates the content displayed by the controller.
+ scoped_ptr<PageInfoModel> model_;
+
+ // Thin bridge that pushes model-changed notifications from C++ to Cocoa.
+ scoped_ptr<PageInfoModel::PageInfoModelObserver> bridge_;
+
+ // The certificate ID for the page, 0 if the page is not over HTTPS.
+ int certID_;
+}
+
+@property (nonatomic, assign) int certID;
+
+// Designated initializer. The new instance will take ownership of |model| and
+// |bridge|. There should be a 1:1 mapping of models to bridges. The
+// controller will release itself when the bubble is closed.
+- (id)initWithPageInfoModel:(PageInfoModel*)model
+ modelObserver:(PageInfoModel::PageInfoModelObserver*)bridge
+ parentWindow:(NSWindow*)parentWindow;
+
+// Shows the certificate display window. Note that this will implicitly close
+// the bubble because the certificate window will become key. The certificate
+// information attaches itself as a sheet to the |parentWindow|.
+- (IBAction)showCertWindow:(id)sender;
+
+// Opens the help center link that explains the contents of the page info.
+- (IBAction)showHelpPage:(id)sender;
+
+@end
+
+@interface PageInfoBubbleController (ExposedForUnitTesting)
+- (void)performLayout;
+@end
diff --git a/chrome/browser/ui/cocoa/page_info_bubble_controller.mm b/chrome/browser/ui/cocoa/page_info_bubble_controller.mm
new file mode 100644
index 0000000..519c759
--- /dev/null
+++ b/chrome/browser/ui/cocoa/page_info_bubble_controller.mm
@@ -0,0 +1,461 @@
+// 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/ui/cocoa/page_info_bubble_controller.h"
+
+#include "app/l10n_util.h"
+#include "app/l10n_util_mac.h"
+#include "base/message_loop.h"
+#include "base/sys_string_conversions.h"
+#include "base/task.h"
+#include "chrome/browser/browser_list.h"
+#include "chrome/browser/cert_store.h"
+#include "chrome/browser/certificate_viewer.h"
+#include "chrome/browser/google/google_util.h"
+#include "chrome/browser/profile.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/hyperlink_button_cell.h"
+#import "chrome/browser/ui/cocoa/info_bubble_view.h"
+#import "chrome/browser/ui/cocoa/info_bubble_window.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
+#include "chrome/common/url_constants.h"
+#include "grit/generated_resources.h"
+#include "grit/locale_settings.h"
+#include "net/base/cert_status_flags.h"
+#include "net/base/x509_certificate.h"
+#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+@interface PageInfoBubbleController (Private)
+- (PageInfoModel*)model;
+- (NSButton*)certificateButtonWithFrame:(NSRect)frame;
+- (void)configureTextFieldAsLabel:(NSTextField*)textField;
+- (CGFloat)addHeadlineViewForInfo:(const PageInfoModel::SectionInfo&)info
+ toSubviews:(NSMutableArray*)subviews
+ atPoint:(NSPoint)point;
+- (CGFloat)addDescriptionViewForInfo:(const PageInfoModel::SectionInfo&)info
+ toSubviews:(NSMutableArray*)subviews
+ atPoint:(NSPoint)point;
+- (CGFloat)addCertificateButtonToSubviews:(NSMutableArray*)subviews
+ atOffset:(CGFloat)offset;
+- (void)addImageViewForInfo:(const PageInfoModel::SectionInfo&)info
+ toSubviews:(NSMutableArray*)subviews
+ atOffset:(CGFloat)offset;
+- (CGFloat)addHelpButtonToSubviews:(NSMutableArray*)subviews
+ atOffset:(CGFloat)offset;
+- (CGFloat)addSeparatorToSubviews:(NSMutableArray*)subviews
+ atOffset:(CGFloat)offset;
+- (NSPoint)anchorPointForWindowWithHeight:(CGFloat)bubbleHeight
+ parentWindow:(NSWindow*)parent;
+@end
+
+// This simple NSView subclass is used as the single subview of the page info
+// bubble's window's contentView. Drawing is flipped so that layout of the
+// sections is easier. Apple recommends flipping the coordinate origin when
+// doing a lot of text layout because it's more natural.
+@interface PageInfoContentView : NSView {
+}
+@end
+@implementation PageInfoContentView
+- (BOOL)isFlipped {
+ return YES;
+}
+@end
+
+namespace {
+
+// The width of the window, in view coordinates. The height will be determined
+// by the content.
+const NSInteger kWindowWidth = 380;
+
+// Spacing in between sections.
+const NSInteger kVerticalSpacing = 10;
+
+// Padding along on the X-axis between the window frame and content.
+const NSInteger kFramePadding = 10;
+
+// Spacing between the optional headline and description text views.
+const NSInteger kHeadlineSpacing = 2;
+
+// Spacing between the image and the text.
+const NSInteger kImageSpacing = 10;
+
+// Square size of the image.
+const CGFloat kImageSize = 30;
+
+// The X position of the text fields. Variants for with and without an image.
+const CGFloat kTextXPositionNoImage = kFramePadding;
+const CGFloat kTextXPosition = kTextXPositionNoImage + kImageSize +
+ kImageSpacing;
+
+// Width of the text fields.
+const CGFloat kTextWidth = kWindowWidth - (kImageSize + kImageSpacing +
+ kFramePadding * 2);
+
+// Bridge that listens for change notifications from the model.
+class PageInfoModelBubbleBridge : public PageInfoModel::PageInfoModelObserver {
+ public:
+ PageInfoModelBubbleBridge()
+ : controller_(nil),
+ ALLOW_THIS_IN_INITIALIZER_LIST(task_factory_(this)) {
+ }
+
+ // PageInfoModelObserver implementation.
+ virtual void ModelChanged() {
+ // Check to see if a layout has already been scheduled.
+ if (!task_factory_.empty())
+ return;
+
+ // Delay performing layout by a second so that all the animations from
+ // InfoBubbleWindow and origin updates from BaseBubbleController finish, so
+ // that we don't all race trying to change the frame's origin.
+ //
+ // Using ScopedRunnableMethodFactory is superior here to |-performSelector:|
+ // because it will not retain its target; if the child outlives its parent,
+ // zombies get left behind (http://crbug.com/59619). This will also cancel
+ // the scheduled Tasks if the controller (and thus this bridge) get
+ // destroyed before the message can be delivered.
+ MessageLoop::current()->PostDelayedTask(FROM_HERE,
+ task_factory_.NewRunnableMethod(
+ &PageInfoModelBubbleBridge::PerformLayout),
+ 1000 /* milliseconds */);
+ }
+
+ // Sets the controller.
+ void set_controller(PageInfoBubbleController* controller) {
+ controller_ = controller;
+ }
+
+ private:
+ void PerformLayout() {
+ [controller_ performLayout];
+ }
+
+ PageInfoBubbleController* controller_; // weak
+
+ // Factory that vends RunnableMethod tasks for scheduling layout.
+ ScopedRunnableMethodFactory<PageInfoModelBubbleBridge> task_factory_;
+};
+
+} // namespace
+
+namespace browser {
+
+void ShowPageInfoBubble(gfx::NativeWindow parent,
+ Profile* profile,
+ const GURL& url,
+ const NavigationEntry::SSLStatus& ssl,
+ bool show_history) {
+ PageInfoModelBubbleBridge* bridge = new PageInfoModelBubbleBridge();
+ PageInfoModel* model =
+ new PageInfoModel(profile, url, ssl, show_history, bridge);
+ PageInfoBubbleController* controller =
+ [[PageInfoBubbleController alloc] initWithPageInfoModel:model
+ modelObserver:bridge
+ parentWindow:parent];
+ bridge->set_controller(controller);
+ [controller setCertID:ssl.cert_id()];
+ [controller showWindow:nil];
+}
+
+} // namespace browser
+
+@implementation PageInfoBubbleController
+
+@synthesize certID = certID_;
+
+- (id)initWithPageInfoModel:(PageInfoModel*)model
+ modelObserver:(PageInfoModel::PageInfoModelObserver*)bridge
+ parentWindow:(NSWindow*)parentWindow {
+ // Use an arbitrary height because it will be changed by the bridge.
+ NSRect contentRect = NSMakeRect(0, 0, kWindowWidth, 0);
+ // Create an empty window into which content is placed.
+ scoped_nsobject<InfoBubbleWindow> window(
+ [[InfoBubbleWindow alloc] initWithContentRect:contentRect
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:NO]);
+
+ if ((self = [super initWithWindow:window.get()
+ parentWindow:parentWindow
+ anchoredAt:NSZeroPoint])) {
+ model_.reset(model);
+ bridge_.reset(bridge);
+ [[self bubble] setArrowLocation:info_bubble::kTopLeft];
+ [self performLayout];
+ }
+ return self;
+}
+
+- (PageInfoModel*)model {
+ return model_.get();
+}
+
+- (IBAction)showCertWindow:(id)sender {
+ DCHECK(certID_ != 0);
+ ShowCertificateViewerByID([self parentWindow], certID_);
+}
+
+- (IBAction)showHelpPage:(id)sender {
+ GURL url = google_util::AppendGoogleLocaleParam(
+ GURL(chrome::kPageInfoHelpCenterURL));
+ Browser* browser = BrowserList::GetLastActive();
+ browser->OpenURL(url, GURL(), NEW_FOREGROUND_TAB, PageTransition::LINK);
+}
+
+// This will create the subviews for the page info window. The general layout
+// is 2 or 3 boxed and titled sections, each of which has a status image to
+// provide visual feedback and a description that explains it. The description
+// text is usually only 1 or 2 lines, but can be much longer. At the bottom of
+// the window is a button to view the SSL certificate, which is disabled if
+// not using HTTPS.
+- (void)performLayout {
+ // |offset| is the Y position that should be drawn at next.
+ CGFloat offset = kFramePadding + info_bubble::kBubbleArrowHeight;
+
+ // Keep the new subviews in an array that gets replaced at the end.
+ NSMutableArray* subviews = [NSMutableArray array];
+
+ // The subviews will be attached to the PageInfoContentView, which has a
+ // flipped origin. This allows the code to build top-to-bottom.
+ const int sectionCount = model_->GetSectionCount();
+ for (int i = 0; i < sectionCount; ++i) {
+ PageInfoModel::SectionInfo info = model_->GetSectionInfo(i);
+
+ // Only certain sections have images. This affects the X position.
+ BOOL hasImage = model_->GetIconImage(info.icon_id) != nil;
+ CGFloat xPosition = (hasImage ? kTextXPosition : kTextXPositionNoImage);
+
+ // Insert the image subview for sections that are appropriate.
+ CGFloat imageBaseline = offset + kImageSize;
+ if (hasImage) {
+ [self addImageViewForInfo:info toSubviews:subviews atOffset:offset];
+ }
+
+ // Add the title.
+ if (!info.headline.empty()) {
+ offset += [self addHeadlineViewForInfo:info
+ toSubviews:subviews
+ atPoint:NSMakePoint(xPosition, offset)];
+ offset += kHeadlineSpacing;
+ }
+
+ // Create the description of the state.
+ offset += [self addDescriptionViewForInfo:info
+ toSubviews:subviews
+ atPoint:NSMakePoint(xPosition, offset)];
+
+ if (info.type == PageInfoModel::SECTION_INFO_IDENTITY && certID_) {
+ offset += kVerticalSpacing;
+ offset += [self addCertificateButtonToSubviews:subviews atOffset:offset];
+ }
+
+ // If at this point the description and optional headline and button are
+ // not as tall as the image, adjust the offset by the difference.
+ CGFloat imageBaselineDelta = imageBaseline - offset;
+ if (imageBaselineDelta > 0)
+ offset += imageBaselineDelta;
+
+ // Add the separators.
+ offset += kVerticalSpacing;
+ offset += [self addSeparatorToSubviews:subviews atOffset:offset];
+ }
+
+ // The last item at the bottom of the window is the help center link.
+ offset += [self addHelpButtonToSubviews:subviews atOffset:offset];
+ offset += kVerticalSpacing;
+
+ // Create the dummy view that uses flipped coordinates.
+ NSRect contentFrame = NSMakeRect(0, 0, kWindowWidth, offset);
+ scoped_nsobject<PageInfoContentView> contentView(
+ [[PageInfoContentView alloc] initWithFrame:contentFrame]);
+ [contentView setSubviews:subviews];
+
+ // Replace the window's content.
+ [[[self window] contentView] setSubviews:
+ [NSArray arrayWithObject:contentView]];
+
+ NSRect windowFrame = NSMakeRect(0, 0, kWindowWidth, offset);
+ windowFrame.size = [[[self window] contentView] convertSize:windowFrame.size
+ toView:nil];
+ // Adjust the origin by the difference in height.
+ windowFrame.origin = [[self window] frame].origin;
+ windowFrame.origin.y -= NSHeight(windowFrame) -
+ NSHeight([[self window] frame]);
+
+ // Resize the window. Only animate if the window is visible, otherwise it
+ // could be "growing" while it's opening, looking awkward.
+ [[self window] setFrame:windowFrame
+ display:YES
+ animate:[[self window] isVisible]];
+
+ NSPoint anchorPoint =
+ [self anchorPointForWindowWithHeight:NSHeight(windowFrame)
+ parentWindow:[self parentWindow]];
+ [self setAnchorPoint:anchorPoint];
+}
+
+// Creates the button with a given |frame| that, when clicked, will show the
+// SSL certificate information.
+- (NSButton*)certificateButtonWithFrame:(NSRect)frame {
+ NSButton* certButton = [[[NSButton alloc] initWithFrame:frame] autorelease];
+ [certButton setTitle:
+ l10n_util::GetNSStringWithFixup(IDS_PAGEINFO_CERT_INFO_BUTTON)];
+ [certButton setButtonType:NSMomentaryPushInButton];
+ [certButton setBezelStyle:NSRoundRectBezelStyle];
+ [certButton setTarget:self];
+ [certButton setAction:@selector(showCertWindow:)];
+ [[certButton cell] setControlSize:NSSmallControlSize];
+ NSFont* font = [NSFont systemFontOfSize:
+ [NSFont systemFontSizeForControlSize:NSSmallControlSize]];
+ [[certButton cell] setFont:font];
+ return certButton;
+}
+
+// Sets proprties on the given |field| to act as the title or description labels
+// in the bubble.
+- (void)configureTextFieldAsLabel:(NSTextField*)textField {
+ [textField setEditable:NO];
+ [textField setDrawsBackground:NO];
+ [textField setBezeled:NO];
+}
+
+// Adds the title text field at the given x,y position, and returns the y
+// position for the next element.
+- (CGFloat)addHeadlineViewForInfo:(const PageInfoModel::SectionInfo&)info
+ toSubviews:(NSMutableArray*)subviews
+ atPoint:(NSPoint)point {
+ NSRect frame = NSMakeRect(point.x, point.y, kTextWidth, kImageSpacing);
+ scoped_nsobject<NSTextField> textField(
+ [[NSTextField alloc] initWithFrame:frame]);
+ [self configureTextFieldAsLabel:textField.get()];
+ [textField setStringValue:base::SysUTF16ToNSString(info.headline)];
+ NSFont* font = [NSFont boldSystemFontOfSize:[NSFont smallSystemFontSize]];
+ [textField setFont:font];
+ frame.size.height +=
+ [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:
+ textField];
+ [textField setFrame:frame];
+ [subviews addObject:textField.get()];
+ return NSHeight(frame);
+}
+
+// Adds the description text field at the given x,y position, and returns the y
+// position for the next element.
+- (CGFloat)addDescriptionViewForInfo:(const PageInfoModel::SectionInfo&)info
+ toSubviews:(NSMutableArray*)subviews
+ atPoint:(NSPoint)point {
+ NSRect frame = NSMakeRect(point.x, point.y, kTextWidth, kImageSize);
+ scoped_nsobject<NSTextField> textField(
+ [[NSTextField alloc] initWithFrame:frame]);
+ [self configureTextFieldAsLabel:textField.get()];
+ [textField setStringValue:base::SysUTF16ToNSString(info.description)];
+ [textField setFont:[NSFont labelFontOfSize:[NSFont smallSystemFontSize]]];
+
+ // If the text is oversized, resize the text field.
+ frame.size.height +=
+ [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:
+ textField];
+ [subviews addObject:textField.get()];
+ return NSHeight(frame);
+}
+
+// Adds the certificate button at a pre-determined x position and the given y.
+// Returns the y position for the next element.
+- (CGFloat)addCertificateButtonToSubviews:(NSMutableArray*)subviews
+ atOffset:(CGFloat)offset {
+ // The certificate button should only be added if there is SSL information.
+ DCHECK(certID_);
+
+ // Create the certificate button. The frame will be fixed up by GTM, so
+ // use arbitrary values.
+ NSRect frame = NSMakeRect(kTextXPosition, offset, 100, 14);
+ NSButton* certButton = [self certificateButtonWithFrame:frame];
+ [subviews addObject:certButton];
+ [GTMUILocalizerAndLayoutTweaker sizeToFitView:certButton];
+
+ // By default, assume that we don't have certificate information to show.
+ scoped_refptr<net::X509Certificate> cert;
+ CertStore::GetSharedInstance()->RetrieveCert(certID_, &cert);
+
+ // Don't bother showing certificates if there isn't one. Gears runs
+ // with no OS root certificate.
+ if (!cert.get() || !cert->os_cert_handle()) {
+ // This should only ever happen in unit tests.
+ [certButton setEnabled:NO];
+ }
+
+ return NSHeight([certButton frame]);
+}
+
+// Adds the state image at a pre-determined x position and the given y. This
+// does not affect the next Y position because the image is placed next to
+// a text field that is larger and accounts for the image's size.
+- (void)addImageViewForInfo:(const PageInfoModel::SectionInfo&)info
+ toSubviews:(NSMutableArray*)subviews
+ atOffset:(CGFloat)offset {
+ NSRect frame = NSMakeRect(kFramePadding, offset, kImageSize,
+ kImageSize);
+ scoped_nsobject<NSImageView> imageView(
+ [[NSImageView alloc] initWithFrame:frame]);
+ [imageView setImageFrameStyle:NSImageFrameNone];
+ [imageView setImage:model_->GetIconImage(info.icon_id)];
+ [subviews addObject:imageView.get()];
+}
+
+// Adds the help center button that explains the icons. Returns the y position
+// delta for the next offset.
+- (CGFloat)addHelpButtonToSubviews:(NSMutableArray*)subviews
+ atOffset:(CGFloat)offset {
+ NSRect frame = NSMakeRect(kFramePadding, offset, 100, 10);
+ scoped_nsobject<NSButton> button([[NSButton alloc] initWithFrame:frame]);
+ NSString* string =
+ l10n_util::GetNSStringWithFixup(IDS_PAGE_INFO_HELP_CENTER_LINK);
+ scoped_nsobject<HyperlinkButtonCell> cell(
+ [[HyperlinkButtonCell alloc] initTextCell:string]);
+ [cell setControlSize:NSSmallControlSize];
+ [button setCell:cell.get()];
+ [button setButtonType:NSMomentaryPushInButton];
+ [button setBezelStyle:NSRegularSquareBezelStyle];
+ [button setTarget:self];
+ [button setAction:@selector(showHelpPage:)];
+ [subviews addObject:button.get()];
+
+ // Call size-to-fit to fixup for the localized string.
+ [GTMUILocalizerAndLayoutTweaker sizeToFitView:button.get()];
+ return NSHeight([button frame]);
+}
+
+// Adds a 1px separator between sections. Returns the y position delta for the
+// next offset.
+- (CGFloat)addSeparatorToSubviews:(NSMutableArray*)subviews
+ atOffset:(CGFloat)offset {
+ const CGFloat kSpacerHeight = 1.0;
+ NSRect frame = NSMakeRect(kFramePadding, offset,
+ kWindowWidth - 2 * kFramePadding, kSpacerHeight);
+ scoped_nsobject<NSBox> spacer([[NSBox alloc] initWithFrame:frame]);
+ [spacer setBoxType:NSBoxSeparator];
+ [spacer setBorderType:NSLineBorder];
+ [spacer setAlphaValue:0.2];
+ [subviews addObject:spacer.get()];
+ return kVerticalSpacing + kSpacerHeight;
+}
+
+// Takes in the bubble's height and the parent window, which should be a
+// BrowserWindow, and gets the proper anchor point for the bubble. The returned
+// point is in screen coordinates.
+- (NSPoint)anchorPointForWindowWithHeight:(CGFloat)bubbleHeight
+ parentWindow:(NSWindow*)parent {
+ BrowserWindowController* controller = [parent windowController];
+ NSPoint origin = NSZeroPoint;
+ if ([controller isKindOfClass:[BrowserWindowController class]]) {
+ LocationBarViewMac* locationBar = [controller locationBarBridge];
+ if (locationBar) {
+ NSPoint bubblePoint = locationBar->GetPageInfoBubblePoint();
+ origin = [parent convertBaseToScreen:bubblePoint];
+ }
+ }
+ return origin;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/page_info_bubble_controller_unittest.mm b/chrome/browser/ui/cocoa/page_info_bubble_controller_unittest.mm
new file mode 100644
index 0000000..50281fe
--- /dev/null
+++ b/chrome/browser/ui/cocoa/page_info_bubble_controller_unittest.mm
@@ -0,0 +1,210 @@
+// 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 "app/l10n_util.h"
+#include "base/scoped_nsobject.h"
+#include "base/string_util.h"
+#include "base/string_number_conversions.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/page_info_model.h"
+#import "chrome/browser/ui/cocoa/hyperlink_button_cell.h"
+#import "chrome/browser/ui/cocoa/page_info_bubble_controller.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "grit/generated_resources.h"
+
+namespace {
+
+class FakeModel : public PageInfoModel {
+ public:
+ FakeModel() : PageInfoModel() {}
+
+ void AddSection(SectionStateIcon icon_id,
+ const string16& headline,
+ const string16& description,
+ SectionInfoType type) {
+ sections_.push_back(SectionInfo(
+ icon_id, headline, description, type));
+ }
+};
+
+class FakeBridge : public PageInfoModel::PageInfoModelObserver {
+ public:
+ void ModelChanged() {}
+};
+
+class PageInfoBubbleControllerTest : public CocoaTest {
+ public:
+ PageInfoBubbleControllerTest() {
+ controller_ = nil;
+ model_ = new FakeModel();
+ }
+
+ virtual void TearDown() {
+ [controller_ close];
+ CocoaTest::TearDown();
+ }
+
+ void CreateBubble() {
+ // The controller cleans up after itself when the window closes.
+ controller_ =
+ [[PageInfoBubbleController alloc] initWithPageInfoModel:model_
+ modelObserver:NULL
+ parentWindow:test_window()];
+ window_ = [controller_ window];
+ [controller_ showWindow:nil];
+ }
+
+ // Checks the controller's window for the requisite subviews in the given
+ // numbers.
+ void CheckWindow(int text_count,
+ int image_count,
+ int spacer_count,
+ int button_count) {
+ // All windows have the help center link and a spacer for it.
+ int link_count = 1;
+ ++spacer_count;
+
+ // The window's only immediate child is an invisible view that has a flipped
+ // coordinate origin. It is into this that all views get placed.
+ NSArray* windowSubviews = [[window_ contentView] subviews];
+ EXPECT_EQ(1U, [windowSubviews count]);
+ NSArray* subviews = [[windowSubviews lastObject] subviews];
+
+ for (NSView* view in subviews) {
+ if ([view isKindOfClass:[NSTextField class]]) {
+ --text_count;
+ } else if ([view isKindOfClass:[NSImageView class]]) {
+ --image_count;
+ } else if ([view isKindOfClass:[NSBox class]]) {
+ --spacer_count;
+ } else if ([view isKindOfClass:[NSButton class]]) {
+ NSButton* button = static_cast<NSButton*>(view);
+ // Every window should have a single link button to the help page.
+ if ([[button cell] isKindOfClass:[HyperlinkButtonCell class]]) {
+ --link_count;
+ CheckButton(button, @selector(showHelpPage:));
+ } else {
+ --button_count;
+ CheckButton(button, @selector(showCertWindow:));
+ }
+ } else {
+ ADD_FAILURE() << "Unknown subview: " << [[view description] UTF8String];
+ }
+ }
+ EXPECT_EQ(0, text_count);
+ EXPECT_EQ(0, image_count);
+ EXPECT_EQ(0, spacer_count);
+ EXPECT_EQ(0, button_count);
+ EXPECT_EQ(0, link_count);
+ EXPECT_EQ([window_ delegate], controller_);
+ }
+
+ // Checks that a button is hooked up correctly.
+ void CheckButton(NSButton* button, SEL action) {
+ EXPECT_EQ(action, [button action]);
+ EXPECT_EQ(controller_, [button target]);
+ EXPECT_TRUE([button stringValue]);
+ }
+
+ BrowserTestHelper helper_;
+
+ PageInfoBubbleController* controller_; // Weak, owns self.
+ FakeModel* model_; // Weak, owned by controller.
+ NSWindow* window_; // Weak, owned by controller.
+};
+
+
+TEST_F(PageInfoBubbleControllerTest, NoHistoryNoSecurity) {
+ model_->AddSection(PageInfoModel::ICON_STATE_ERROR,
+ string16(),
+ l10n_util::GetStringUTF16(IDS_PAGE_INFO_SECURITY_TAB_UNKNOWN_PARTY),
+ PageInfoModel::SECTION_INFO_IDENTITY);
+ model_->AddSection(PageInfoModel::ICON_STATE_ERROR,
+ string16(),
+ l10n_util::GetStringFUTF16(
+ IDS_PAGE_INFO_SECURITY_TAB_NOT_ENCRYPTED_CONNECTION_TEXT,
+ ASCIIToUTF16("google.com")),
+ PageInfoModel::SECTION_INFO_CONNECTION);
+
+ CreateBubble();
+ CheckWindow(/*text=*/2, /*image=*/2, /*spacer=*/1, /*button=*/0);
+}
+
+
+TEST_F(PageInfoBubbleControllerTest, HistoryNoSecurity) {
+ model_->AddSection(PageInfoModel::ICON_STATE_ERROR,
+ string16(),
+ l10n_util::GetStringUTF16(IDS_PAGE_INFO_SECURITY_TAB_UNKNOWN_PARTY),
+ PageInfoModel::SECTION_INFO_IDENTITY);
+ model_->AddSection(PageInfoModel::ICON_STATE_ERROR,
+ string16(),
+ l10n_util::GetStringFUTF16(
+ IDS_PAGE_INFO_SECURITY_TAB_NOT_ENCRYPTED_CONNECTION_TEXT,
+ ASCIIToUTF16("google.com")),
+ PageInfoModel::SECTION_INFO_CONNECTION);
+
+ // In practice, the history information comes later because it's queried
+ // asynchronously, so replicate the double-build here.
+ CreateBubble();
+
+ model_->AddSection(PageInfoModel::ICON_STATE_ERROR,
+ l10n_util::GetStringUTF16(IDS_PAGE_INFO_SITE_INFO_TITLE),
+ l10n_util::GetStringUTF16(
+ IDS_PAGE_INFO_SECURITY_TAB_FIRST_VISITED_TODAY),
+ PageInfoModel::SECTION_INFO_FIRST_VISIT);
+
+ [controller_ performLayout];
+
+ CheckWindow(/*text=*/4, /*image=*/3, /*spacer=*/2, /*button=*/0);
+}
+
+
+TEST_F(PageInfoBubbleControllerTest, NoHistoryMixedSecurity) {
+ model_->AddSection(PageInfoModel::ICON_STATE_OK,
+ string16(),
+ l10n_util::GetStringFUTF16(
+ IDS_PAGE_INFO_SECURITY_TAB_SECURE_IDENTITY,
+ ASCIIToUTF16("Goat Security Systems")),
+ PageInfoModel::SECTION_INFO_IDENTITY);
+
+ // This string is super long and the text should overflow the default clip
+ // region (kImageSize).
+ string16 description = l10n_util::GetStringFUTF16(
+ IDS_PAGE_INFO_SECURITY_TAB_ENCRYPTED_SENTENCE_LINK,
+ l10n_util::GetStringFUTF16(
+ IDS_PAGE_INFO_SECURITY_TAB_ENCRYPTED_CONNECTION_TEXT,
+ ASCIIToUTF16("chrome.google.com"),
+ base::IntToString16(1024)),
+ l10n_util::GetStringUTF16(
+ IDS_PAGE_INFO_SECURITY_TAB_ENCRYPTED_INSECURE_CONTENT_WARNING));
+
+ model_->AddSection(PageInfoModel::ICON_STATE_OK,
+ string16(),
+ description,
+ PageInfoModel::SECTION_INFO_CONNECTION);
+
+
+ CreateBubble();
+ [controller_ setCertID:1];
+ [controller_ performLayout];
+
+ CheckWindow(/*text=*/2, /*image=*/2, /*spacer=*/1, /*button=*/1);
+
+ // Look for the over-sized box.
+ NSString* targetDesc = base::SysUTF16ToNSString(description);
+ NSArray* subviews = [[window_ contentView] subviews];
+ for (NSView* subview in subviews) {
+ if ([subview isKindOfClass:[NSTextField class]]) {
+ NSTextField* desc = static_cast<NSTextField*>(subview);
+ if ([[desc stringValue] isEqualToString:targetDesc]) {
+ // Typical box frame is ~55px, make sure this is extra large.
+ EXPECT_LT(75, NSHeight([desc frame]));
+ }
+ }
+ }
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/preferences_window_controller.h b/chrome/browser/ui/cocoa/preferences_window_controller.h
new file mode 100644
index 0000000..cfec955
--- /dev/null
+++ b/chrome/browser/ui/cocoa/preferences_window_controller.h
@@ -0,0 +1,241 @@
+// 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_ptr.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/options_window.h"
+#include "chrome/browser/prefs/pref_member.h"
+#include "chrome/browser/prefs/pref_set_observer.h"
+#include "chrome/browser/prefs/pref_change_registrar.h"
+
+namespace PreferencesWindowControllerInternal {
+class PrefObserverBridge;
+class ManagedPrefsBannerState;
+}
+
+@class CustomHomePagesModel;
+@class FontLanguageSettingsController;
+class PrefService;
+class Profile;
+class ProfileSyncService;
+@class SearchEngineListModel;
+@class VerticalGradientView;
+@class WindowSizeAutosaver;
+
+// A window controller that handles the preferences window. The bulk of the
+// work is handled via Cocoa Bindings and getter/setter methods that wrap
+// cross-platform PrefMember objects. When prefs change in the back-end
+// (that is, outside of this UI), our observer receives a notification and can
+// tickle the KVO to update the UI so we are always in sync. The bindings are
+// specified in the nib file. Preferences are persisted into the back-end
+// as they are changed in the UI, and are thus immediately available even while
+// the window is still open. When the window closes, a notification is sent
+// via the system NotificationCenter. This can be used as a signal to
+// release this controller, as it's likely the client wants to enforce there
+// only being one (we don't do that internally as it makes it very difficult
+// to unit test).
+@interface PreferencesWindowController : NSWindowController {
+ @private
+ Profile* profile_; // weak ref
+ OptionsPage initialPage_;
+ PrefService* prefs_; // weak ref - Obtained from profile_ for convenience.
+ // weak ref - Also obtained from profile_ for convenience. May be NULL.
+ ProfileSyncService* syncService_;
+ scoped_ptr<PreferencesWindowControllerInternal::PrefObserverBridge>
+ observer_; // Watches for pref changes.
+ PrefChangeRegistrar registrar_; // Manages pref change observer registration.
+ scoped_nsobject<WindowSizeAutosaver> sizeSaver_;
+ NSView* currentPrefsView_; // weak ref - current prefs page view.
+ scoped_ptr<PreferencesWindowControllerInternal::ManagedPrefsBannerState>
+ bannerState_;
+ BOOL managedPrefsBannerVisible_;
+
+ IBOutlet NSToolbar* toolbar_;
+ IBOutlet VerticalGradientView* managedPrefsBannerView_;
+ IBOutlet NSImageView* managedPrefsBannerWarningImage_;
+
+ // The views we'll rotate through
+ IBOutlet NSView* basicsView_;
+ IBOutlet NSView* personalStuffView_;
+ IBOutlet NSView* underTheHoodView_;
+ // The last page the user was on when they opened the Options window.
+ IntegerPrefMember lastSelectedPage_;
+
+ // The groups of the Basics view for layout fixup.
+ IBOutlet NSArray* basicsGroupStartup_;
+ IBOutlet NSArray* basicsGroupHomePage_;
+ IBOutlet NSArray* basicsGroupToolbar_;
+ IBOutlet NSArray* basicsGroupSearchEngine_;
+ IBOutlet NSArray* basicsGroupDefaultBrowser_;
+
+ // The groups of the Personal Stuff view for layout fixup.
+ IBOutlet NSArray* personalStuffGroupSync_;
+ IBOutlet NSArray* personalStuffGroupPasswords_;
+ IBOutlet NSArray* personalStuffGroupAutofill_;
+ IBOutlet NSArray* personalStuffGroupBrowserData_;
+ IBOutlet NSArray* personalStuffGroupThemes_;
+
+ // Having two animations around is bad (they fight), so just use one.
+ scoped_nsobject<NSViewAnimation> animation_;
+
+ IBOutlet NSArrayController* customPagesArrayController_;
+
+ // Basics panel
+ IntegerPrefMember restoreOnStartup_;
+ scoped_nsobject<CustomHomePagesModel> customPagesSource_;
+ BooleanPrefMember newTabPageIsHomePage_;
+ StringPrefMember homepage_;
+ BooleanPrefMember showHomeButton_;
+ BooleanPrefMember instantEnabled_;
+ IBOutlet NSButton* instantCheckbox_;
+ IBOutlet NSTextField* instantExperiment_;
+ scoped_nsobject<SearchEngineListModel> searchEngineModel_;
+ // Used when creating a new home page url to make the new cell editable.
+ BOOL pendingSelectForEdit_;
+ BOOL restoreButtonsEnabled_;
+ BOOL restoreURLsEnabled_;
+ BOOL showHomeButtonEnabled_;
+ BOOL defaultSearchEngineEnabled_;
+
+ // User Data panel
+ BooleanPrefMember askSavePasswords_;
+ BooleanPrefMember autoFillEnabled_;
+ IBOutlet NSButton* autoFillSettingsButton_;
+ IBOutlet NSButton* syncButton_;
+ IBOutlet NSButton* syncCustomizeButton_;
+ IBOutlet NSTextField* syncStatus_;
+ IBOutlet NSButton* syncLink_;
+ IBOutlet NSButton* privacyDashboardLink_;
+ scoped_nsobject<NSColor> syncStatusNoErrorBackgroundColor_;
+ scoped_nsobject<NSColor> syncLinkNoErrorBackgroundColor_;
+ scoped_nsobject<NSColor> syncErrorBackgroundColor_;
+ BOOL passwordManagerChoiceEnabled_;
+ BOOL passwordManagerButtonEnabled_;
+ BOOL autoFillSettingsButtonEnabled_;
+
+ // Under the hood panel
+ IBOutlet NSView* underTheHoodContentView_;
+ IBOutlet NSScrollView* underTheHoodScroller_;
+ IBOutlet NSButton* contentSettingsButton_;
+ IBOutlet NSButton* clearDataButton_;
+ BooleanPrefMember alternateErrorPages_;
+ BooleanPrefMember useSuggest_;
+ BooleanPrefMember dnsPrefetch_;
+ BooleanPrefMember safeBrowsing_;
+ BooleanPrefMember metricsReporting_;
+ IBOutlet NSPathControl* downloadLocationControl_;
+ IBOutlet NSButton* downloadLocationButton_;
+ StringPrefMember defaultDownloadLocation_;
+ BooleanPrefMember askForSaveLocation_;
+ IBOutlet NSButton* resetFileHandlersButton_;
+ StringPrefMember autoOpenFiles_;
+ BooleanPrefMember translateEnabled_;
+ BooleanPrefMember tabsToLinks_;
+ FontLanguageSettingsController* fontLanguageSettings_;
+ StringPrefMember currentTheme_;
+ IBOutlet NSButton* enableLoggingCheckbox_;
+ scoped_ptr<PrefSetObserver> proxyPrefs_;
+ BOOL showAlternateErrorPagesEnabled_;
+ BOOL useSuggestEnabled_;
+ BOOL dnsPrefetchEnabled_;
+ BOOL safeBrowsingEnabled_;
+ BOOL metricsReportingEnabled_;
+ BOOL proxiesConfigureButtonEnabled_;
+}
+
+// Designated initializer. |profile| should not be NULL.
+- (id)initWithProfile:(Profile*)profile initialPage:(OptionsPage)initialPage;
+
+// Show the preferences window.
+- (void)showPreferences:(id)sender;
+
+// Switch to the given preference page.
+- (void)switchToPage:(OptionsPage)page animate:(BOOL)animate;
+
+// Enables or disables the restoreOnStartup elements
+- (void) setEnabledStateOfRestoreOnStartup;
+
+// IBAction methods for responding to user actions.
+
+// Basics panel
+- (IBAction)addHomepage:(id)sender;
+- (IBAction)removeSelectedHomepages:(id)sender;
+- (IBAction)useCurrentPagesAsHomepage:(id)sender;
+- (IBAction)manageSearchEngines:(id)sender;
+- (IBAction)toggleInstant:(id)sender;
+- (IBAction)learnMoreAboutInstant:(id)sender;
+- (IBAction)makeDefaultBrowser:(id)sender;
+
+// User Data panel
+- (IBAction)doSyncAction:(id)sender;
+- (IBAction)doSyncCustomize:(id)sender;
+- (IBAction)doSyncReauthentication:(id)sender;
+- (IBAction)showPrivacyDashboard:(id)sender;
+- (IBAction)showSavedPasswords:(id)sender;
+- (IBAction)showAutoFillSettings:(id)sender;
+- (IBAction)importData:(id)sender;
+- (IBAction)resetThemeToDefault:(id)sender;
+- (IBAction)themesGallery:(id)sender;
+
+// Under the hood
+- (IBAction)showContentSettings:(id)sender;
+- (IBAction)clearData:(id)sender;
+- (IBAction)privacyLearnMore:(id)sender;
+- (IBAction)browseDownloadLocation:(id)sender;
+- (IBAction)resetAutoOpenFiles:(id)sender;
+- (IBAction)changeFontAndLanguageSettings:(id)sender;
+- (IBAction)openProxyPreferences:(id)sender;
+- (IBAction)showCertificates:(id)sender;
+- (IBAction)resetToDefaults:(id)sender;
+
+// When a toolbar button is clicked
+- (IBAction)toolbarButtonSelected:(id)sender;
+
+// Usable from cocoa bindings to hook up the custom home pages table.
+@property (nonatomic, readonly) CustomHomePagesModel* customPagesSource;
+
+// Properties for the enabled state of various UI elements. Keep these ordered
+// by occurrence on the dialog.
+@property (nonatomic) BOOL restoreButtonsEnabled;
+@property (nonatomic) BOOL restoreURLsEnabled;
+@property (nonatomic) BOOL showHomeButtonEnabled;
+@property (nonatomic) BOOL defaultSearchEngineEnabled;
+@property (nonatomic) BOOL passwordManagerChoiceEnabled;
+@property (nonatomic) BOOL passwordManagerButtonEnabled;
+@property (nonatomic) BOOL autoFillSettingsButtonEnabled;
+@property (nonatomic) BOOL showAlternateErrorPagesEnabled;
+@property (nonatomic) BOOL useSuggestEnabled;
+@property (nonatomic) BOOL dnsPrefetchEnabled;
+@property (nonatomic) BOOL safeBrowsingEnabled;
+@property (nonatomic) BOOL metricsReportingEnabled;
+@property (nonatomic) BOOL proxiesConfigureButtonEnabled;
+@end
+
+@interface PreferencesWindowController(Testing)
+
+- (IntegerPrefMember*)lastSelectedPage;
+- (NSToolbar*)toolbar;
+- (NSView*)basicsView;
+- (NSView*)personalStuffView;
+- (NSView*)underTheHoodView;
+
+// Converts the given OptionsPage value (which may be OPTIONS_PAGE_DEFAULT)
+// into a concrete OptionsPage value.
+- (OptionsPage)normalizePage:(OptionsPage)page;
+
+// Returns the toolbar item corresponding to the given page. Should be
+// called only after awakeFromNib is.
+- (NSToolbarItem*)getToolbarItemForPage:(OptionsPage)page;
+
+// Returns the (normalized) page corresponding to the given toolbar item.
+// Should be called only after awakeFromNib is.
+- (OptionsPage)getPageForToolbarItem:(NSToolbarItem*)toolbarItem;
+
+// Returns the view corresponding to the given page. Should be called
+// only after awakeFromNib is.
+- (NSView*)getPrefsViewForPage:(OptionsPage)page;
+
+@end
diff --git a/chrome/browser/ui/cocoa/preferences_window_controller.mm b/chrome/browser/ui/cocoa/preferences_window_controller.mm
new file mode 100644
index 0000000..de8459a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/preferences_window_controller.mm
@@ -0,0 +1,2184 @@
+// 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/ui/cocoa/preferences_window_controller.h"
+
+#include <algorithm>
+
+#include "app/l10n_util.h"
+#include "app/l10n_util_mac.h"
+#include "app/resource_bundle.h"
+#include "base/logging.h"
+#include "base/mac_util.h"
+#include "base/mac/scoped_aedesc.h"
+#include "base/string16.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/autofill/autofill_dialog.h"
+#include "chrome/browser/autofill/autofill_type.h"
+#include "chrome/browser/autofill/personal_data_manager.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/download/download_manager.h"
+#include "chrome/browser/download/download_prefs.h"
+#include "chrome/browser/extensions/extensions_service.h"
+#include "chrome/browser/google/google_util.h"
+#include "chrome/browser/instant/instant_confirm_dialog.h"
+#include "chrome/browser/instant/instant_controller.h"
+#include "chrome/browser/metrics/metrics_service.h"
+#include "chrome/browser/metrics/user_metrics.h"
+#include "chrome/browser/net/url_fixer_upper.h"
+#include "chrome/browser/options_util.h"
+#include "chrome/browser/options_window.h"
+#include "chrome/browser/policy/managed_prefs_banner_base.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/prefs/session_startup_pref.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/safe_browsing/safe_browsing_service.h"
+#include "chrome/browser/shell_integration.h"
+#include "chrome/browser/show_options_url.h"
+#include "chrome/browser/sync/profile_sync_service.h"
+#include "chrome/browser/sync/sync_ui_util.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list.h"
+#import "chrome/browser/ui/cocoa/clear_browsing_data_controller.h"
+#import "chrome/browser/ui/cocoa/content_settings_dialog_controller.h"
+#import "chrome/browser/ui/cocoa/custom_home_pages_model.h"
+#import "chrome/browser/ui/cocoa/font_language_settings_controller.h"
+#import "chrome/browser/ui/cocoa/import_settings_dialog.h"
+#import "chrome/browser/ui/cocoa/keyword_editor_cocoa_controller.h"
+#import "chrome/browser/ui/cocoa/l10n_util.h"
+#import "chrome/browser/ui/cocoa/search_engine_list_model.h"
+#import "chrome/browser/ui/cocoa/vertical_gradient_view.h"
+#import "chrome/browser/ui/cocoa/window_size_autosaver.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/common/notification_details.h"
+#include "chrome/common/notification_observer.h"
+#include "chrome/common/notification_type.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/url_constants.h"
+#include "chrome/installer/util/google_update_settings.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+#include "grit/locale_settings.h"
+#include "grit/theme_resources.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+namespace {
+
+// Colors for the managed preferences warning banner.
+static const double kBannerGradientColorTop[3] =
+ {255.0 / 255.0, 242.0 / 255.0, 183.0 / 255.0};
+static const double kBannerGradientColorBottom[3] =
+ {250.0 / 255.0, 230.0 / 255.0, 145.0 / 255.0};
+static const double kBannerStrokeColor = 135.0 / 255.0;
+
+// Tag id for retrieval via viewWithTag in NSView (from IB).
+static const uint32 kBasicsStartupPageTableTag = 1000;
+
+bool IsNewTabUIURLString(const GURL& url) {
+ return url == GURL(chrome::kChromeUINewTabURL);
+}
+
+// Helper that sizes two buttons to fit in a row keeping their spacing, returns
+// the total horizontal size change.
+CGFloat SizeToFitButtonPair(NSButton* leftButton, NSButton* rightButton) {
+ CGFloat widthShift = 0.0;
+
+ NSSize delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:leftButton];
+ DCHECK_EQ(delta.height, 0.0) << "Height changes unsupported";
+ widthShift += delta.width;
+
+ if (widthShift != 0.0) {
+ NSPoint origin = [rightButton frame].origin;
+ origin.x += widthShift;
+ [rightButton setFrameOrigin:origin];
+ }
+ delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:rightButton];
+ DCHECK_EQ(delta.height, 0.0) << "Height changes unsupported";
+ widthShift += delta.width;
+
+ return widthShift;
+}
+
+// The different behaviors for the "pref group" auto sizing.
+enum AutoSizeGroupBehavior {
+ kAutoSizeGroupBehaviorVerticalToFit,
+ kAutoSizeGroupBehaviorVerticalFirstToFit,
+ kAutoSizeGroupBehaviorHorizontalToFit,
+ kAutoSizeGroupBehaviorHorizontalFirstGrows,
+ kAutoSizeGroupBehaviorFirstTwoAsRowVerticalToFit
+};
+
+// Helper to tweak the layout of the "pref groups" and also ripple any height
+// changes from one group to the next groups' origins.
+// |views| is an ordered list of views with first being the label for the
+// group and the rest being top down or left to right ordering of the views.
+// The label is assumed to already be the same height as all the views it is
+// next too.
+CGFloat AutoSizeGroup(NSArray* views, AutoSizeGroupBehavior behavior,
+ CGFloat verticalShift) {
+ DCHECK_GE([views count], 2U) << "Should be at least a label and a control";
+ NSTextField* label = [views objectAtIndex:0];
+ DCHECK([label isKindOfClass:[NSTextField class]])
+ << "First view should be the label for the group";
+
+ // Auto size the label to see if we need more vertical space for its localized
+ // string.
+ CGFloat labelHeightChange =
+ [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:label];
+
+ CGFloat localVerticalShift = 0.0;
+ switch (behavior) {
+ case kAutoSizeGroupBehaviorVerticalToFit: {
+ // Walk bottom up doing the sizing and moves.
+ for (NSUInteger index = [views count] - 1; index > 0; --index) {
+ NSView* view = [views objectAtIndex:index];
+ NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view);
+ DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height";
+ if (localVerticalShift) {
+ NSPoint origin = [view frame].origin;
+ origin.y += localVerticalShift;
+ [view setFrameOrigin:origin];
+ }
+ localVerticalShift += delta.height;
+ }
+ break;
+ }
+ case kAutoSizeGroupBehaviorVerticalFirstToFit: {
+ // Just size the top one.
+ NSView* view = [views objectAtIndex:1];
+ NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view);
+ DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height";
+ localVerticalShift += delta.height;
+ break;
+ }
+ case kAutoSizeGroupBehaviorHorizontalToFit: {
+ // Walk left to right doing the sizing and moves.
+ // NOTE: Don't worry about vertical, assume it always fits.
+ CGFloat horizontalShift = 0.0;
+ NSUInteger count = [views count];
+ for (NSUInteger index = 1; index < count; ++index) {
+ NSView* view = [views objectAtIndex:index];
+ NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view);
+ DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height";
+ if (horizontalShift) {
+ NSPoint origin = [view frame].origin;
+ origin.x += horizontalShift;
+ [view setFrameOrigin:origin];
+ }
+ horizontalShift += delta.width;
+ }
+ break;
+ }
+ case kAutoSizeGroupBehaviorHorizontalFirstGrows: {
+ // Walk right to left doing the sizing and moves, then apply the space
+ // collected into the first.
+ // NOTE: Don't worry about vertical, assume it always all fits.
+ CGFloat horizontalShift = 0.0;
+ for (NSUInteger index = [views count] - 1; index > 1; --index) {
+ NSView* view = [views objectAtIndex:index];
+ NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view);
+ DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height";
+ horizontalShift -= delta.width;
+ NSPoint origin = [view frame].origin;
+ origin.x += horizontalShift;
+ [view setFrameOrigin:origin];
+ }
+ if (horizontalShift) {
+ NSView* view = [views objectAtIndex:1];
+ NSSize delta = NSMakeSize(horizontalShift, 0.0);
+ [GTMUILocalizerAndLayoutTweaker
+ resizeViewWithoutAutoResizingSubViews:view
+ delta:delta];
+ }
+ break;
+ }
+ case kAutoSizeGroupBehaviorFirstTwoAsRowVerticalToFit: {
+ // Start out like kAutoSizeGroupBehaviorVerticalToFit but don't do
+ // the first two. Then handle the two as a row, but apply any
+ // vertical shift.
+ // All but the first two (in the row); walk bottom up.
+ for (NSUInteger index = [views count] - 1; index > 2; --index) {
+ NSView* view = [views objectAtIndex:index];
+ NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view);
+ DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height";
+ if (localVerticalShift) {
+ NSPoint origin = [view frame].origin;
+ origin.y += localVerticalShift;
+ [view setFrameOrigin:origin];
+ }
+ localVerticalShift += delta.height;
+ }
+ // Deal with the two for the horizontal row. Size the second one.
+ CGFloat horizontalShift = 0.0;
+ NSView* view = [views objectAtIndex:2];
+ NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view);
+ DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height";
+ horizontalShift -= delta.width;
+ NSPoint origin = [view frame].origin;
+ origin.x += horizontalShift;
+ if (localVerticalShift) {
+ origin.y += localVerticalShift;
+ }
+ [view setFrameOrigin:origin];
+ // Now expand the first item in the row to consume the space opened up.
+ view = [views objectAtIndex:1];
+ if (horizontalShift) {
+ NSSize delta = NSMakeSize(horizontalShift, 0.0);
+ [GTMUILocalizerAndLayoutTweaker
+ resizeViewWithoutAutoResizingSubViews:view
+ delta:delta];
+ }
+ // And move it up by any amount needed from the previous items.
+ if (localVerticalShift) {
+ NSPoint origin = [view frame].origin;
+ origin.y += localVerticalShift;
+ [view setFrameOrigin:origin];
+ }
+ break;
+ }
+ default:
+ NOTREACHED();
+ break;
+ }
+
+ // If the label grew more then the views, the other views get an extra shift.
+ // Otherwise, move the label to its top is aligned with the other views.
+ CGFloat nonLabelShift = 0.0;
+ if (labelHeightChange > localVerticalShift) {
+ // Since the lable is taller, centering the other views looks best, just
+ // shift the views by 1/2 of the size difference.
+ nonLabelShift = (labelHeightChange - localVerticalShift) / 2.0;
+ } else {
+ NSPoint origin = [label frame].origin;
+ origin.y += localVerticalShift - labelHeightChange;
+ [label setFrameOrigin:origin];
+ }
+
+ // Apply the input shift requested along with any the shift from label being
+ // taller then the rest of the group.
+ for (NSView* view in views) {
+ NSPoint origin = [view frame].origin;
+ origin.y += verticalShift;
+ if (view != label) {
+ origin.y += nonLabelShift;
+ }
+ [view setFrameOrigin:origin];
+ }
+
+ // Return how much the group grew.
+ return localVerticalShift + nonLabelShift;
+}
+
+// Helper to remove a view and move everything above it down to take over the
+// space.
+void RemoveViewFromView(NSView* view, NSView* toRemove) {
+ // Sort bottom up so we can spin over what is above it.
+ NSArray* views =
+ [[view subviews] sortedArrayUsingFunction:cocoa_l10n_util::CompareFrameY
+ context:NULL];
+
+ // Find where |toRemove| was.
+ NSUInteger index = [views indexOfObject:toRemove];
+ DCHECK_NE(index, NSNotFound);
+ NSUInteger count = [views count];
+ CGFloat shrinkHeight = 0;
+ if (index < (count - 1)) {
+ // If we're not the topmost control, the amount to shift is the bottom of
+ // |toRemove| to the bottom of the view above it.
+ CGFloat shiftDown =
+ NSMinY([[views objectAtIndex:index + 1] frame]) -
+ NSMinY([toRemove frame]);
+
+ // Now cycle over the views above it moving them down.
+ for (++index; index < count; ++index) {
+ NSView* view = [views objectAtIndex:index];
+ NSPoint origin = [view frame].origin;
+ origin.y -= shiftDown;
+ [view setFrameOrigin:origin];
+ }
+
+ shrinkHeight = shiftDown;
+ } else if (index > 0) {
+ // If we're the topmost control, there's nothing to shift but we want to
+ // shrink until the top edge of the second-topmost control, unless it is
+ // actually higher than the topmost control (since we're sorting by the
+ // bottom edge).
+ shrinkHeight = std::max(0.f,
+ NSMaxY([toRemove frame]) -
+ NSMaxY([[views objectAtIndex:index - 1] frame]));
+ }
+ // If we only have one control, don't do any resizing (for now).
+
+ // Remove |toRemove|.
+ [toRemove removeFromSuperview];
+
+ [GTMUILocalizerAndLayoutTweaker
+ resizeViewWithoutAutoResizingSubViews:view
+ delta:NSMakeSize(0, -shrinkHeight)];
+}
+
+// Simply removes all the views in |toRemove|.
+void RemoveGroupFromView(NSView* view, NSArray* toRemove) {
+ for (NSView* viewToRemove in toRemove) {
+ RemoveViewFromView(view, viewToRemove);
+ }
+}
+
+// Helper to tweak the layout of the "Under the Hood" content by autosizing all
+// the views and moving things up vertically. Special case the two controls for
+// download location as they are horizontal, and should fill the row. Special
+// case "Content Settings" and "Clear browsing data" as they are horizontal as
+// well.
+CGFloat AutoSizeUnderTheHoodContent(NSView* view,
+ NSPathControl* downloadLocationControl,
+ NSButton* downloadLocationButton) {
+ CGFloat verticalShift = 0.0;
+
+ // Loop bottom up through the views sizing and shifting.
+ NSArray* views =
+ [[view subviews] sortedArrayUsingFunction:cocoa_l10n_util::CompareFrameY
+ context:NULL];
+ for (NSView* view in views) {
+ NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view);
+ DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height";
+ if (verticalShift) {
+ NSPoint origin = [view frame].origin;
+ origin.y += verticalShift;
+ [view setFrameOrigin:origin];
+ }
+ verticalShift += delta.height;
+
+ // The Download Location controls go in a row with the button aligned to the
+ // right edge and the path control using all the rest of the space.
+ if (view == downloadLocationButton) {
+ NSPoint origin = [downloadLocationButton frame].origin;
+ origin.x -= delta.width;
+ [downloadLocationButton setFrameOrigin:origin];
+ NSSize controlSize = [downloadLocationControl frame].size;
+ controlSize.width -= delta.width;
+ [downloadLocationControl setFrameSize:controlSize];
+ }
+ }
+
+ return verticalShift;
+}
+
+} // namespace
+
+//-------------------------------------------------------------------------
+
+@interface PreferencesWindowController(Private)
+// Callback when preferences are changed. |prefName| is the name of the
+// pref that has changed.
+- (void)prefChanged:(std::string*)prefName;
+// Callback when sync state has changed. syncService_ needs to be
+// queried to find out what happened.
+- (void)syncStateChanged;
+// Record the user performed a certain action and save the preferences.
+- (void)recordUserAction:(const UserMetricsAction&) action;
+- (void)registerPrefObservers;
+- (void)configureInstant;
+
+// KVC setter methods.
+- (void)setNewTabPageIsHomePageIndex:(NSInteger)val;
+- (void)setHomepageURL:(NSString*)urlString;
+- (void)setRestoreOnStartupIndex:(NSInteger)type;
+- (void)setShowHomeButton:(BOOL)value;
+- (void)setPasswordManagerEnabledIndex:(NSInteger)value;
+- (void)setIsUsingDefaultTheme:(BOOL)value;
+- (void)setShowAlternateErrorPages:(BOOL)value;
+- (void)setUseSuggest:(BOOL)value;
+- (void)setDnsPrefetch:(BOOL)value;
+- (void)setSafeBrowsing:(BOOL)value;
+- (void)setMetricsReporting:(BOOL)value;
+- (void)setAskForSaveLocation:(BOOL)value;
+- (void)setFileHandlerUIEnabled:(BOOL)value;
+- (void)setTranslateEnabled:(BOOL)value;
+- (void)setTabsToLinks:(BOOL)value;
+- (void)displayPreferenceViewForPage:(OptionsPage)page
+ animate:(BOOL)animate;
+- (void)resetSubViews;
+- (void)initBannerStateForPage:(OptionsPage)page;
+
+// KVC getter methods.
+- (BOOL)fileHandlerUIEnabled;
+@end
+
+namespace PreferencesWindowControllerInternal {
+
+// A C++ class registered for changes in preferences. Bridges the
+// notification back to the PWC.
+class PrefObserverBridge : public NotificationObserver,
+ public ProfileSyncServiceObserver {
+ public:
+ PrefObserverBridge(PreferencesWindowController* controller)
+ : controller_(controller) {}
+
+ virtual ~PrefObserverBridge() {}
+
+ // Overridden from NotificationObserver:
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ if (type == NotificationType::PREF_CHANGED)
+ [controller_ prefChanged:Details<std::string>(details).ptr()];
+ }
+
+ // Overridden from ProfileSyncServiceObserver.
+ virtual void OnStateChanged() {
+ [controller_ syncStateChanged];
+ }
+
+ private:
+ PreferencesWindowController* controller_; // weak, owns us
+};
+
+// Tracks state for a managed prefs banner and triggers UI updates through the
+// PreferencesWindowController as appropriate.
+class ManagedPrefsBannerState : public policy::ManagedPrefsBannerBase {
+ public:
+ virtual ~ManagedPrefsBannerState() { }
+
+ explicit ManagedPrefsBannerState(PreferencesWindowController* controller,
+ OptionsPage page,
+ PrefService* local_state,
+ PrefService* prefs)
+ : policy::ManagedPrefsBannerBase(local_state, prefs, page),
+ controller_(controller),
+ page_(page) { }
+
+ BOOL IsVisible() {
+ return DetermineVisibility();
+ }
+
+ protected:
+ // Overridden from ManagedPrefsBannerBase.
+ virtual void OnUpdateVisibility() {
+ [controller_ switchToPage:page_ animate:YES];
+ }
+
+ private:
+ PreferencesWindowController* controller_; // weak, owns us
+ OptionsPage page_; // current options page
+};
+
+} // namespace PreferencesWindowControllerInternal
+
+@implementation PreferencesWindowController
+
+@synthesize restoreButtonsEnabled = restoreButtonsEnabled_;
+@synthesize restoreURLsEnabled = restoreURLsEnabled_;
+@synthesize showHomeButtonEnabled = showHomeButtonEnabled_;
+@synthesize defaultSearchEngineEnabled = defaultSearchEngineEnabled_;
+@synthesize passwordManagerChoiceEnabled = passwordManagerChoiceEnabled_;
+@synthesize passwordManagerButtonEnabled = passwordManagerButtonEnabled_;
+@synthesize autoFillSettingsButtonEnabled = autoFillSettingsButtonEnabled_;
+@synthesize showAlternateErrorPagesEnabled = showAlternateErrorPagesEnabled_;
+@synthesize useSuggestEnabled = useSuggestEnabled_;
+@synthesize dnsPrefetchEnabled = dnsPrefetchEnabled_;
+@synthesize safeBrowsingEnabled = safeBrowsingEnabled_;
+@synthesize metricsReportingEnabled = metricsReportingEnabled_;
+@synthesize proxiesConfigureButtonEnabled = proxiesConfigureButtonEnabled_;
+
+- (id)initWithProfile:(Profile*)profile initialPage:(OptionsPage)initialPage {
+ DCHECK(profile);
+ // Use initWithWindowNibPath:: instead of initWithWindowNibName: so we
+ // can override it in a unit test.
+ NSString* nibPath = [mac_util::MainAppBundle()
+ pathForResource:@"Preferences"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
+ profile_ = profile->GetOriginalProfile();
+ initialPage_ = initialPage;
+ prefs_ = profile->GetPrefs();
+ DCHECK(prefs_);
+ observer_.reset(
+ new PreferencesWindowControllerInternal::PrefObserverBridge(self));
+
+ // Set up the model for the custom home page table. The KVO observation
+ // tells us when the number of items in the array changes. The normal
+ // observation tells us when one of the URLs of an item changes.
+ customPagesSource_.reset([[CustomHomePagesModel alloc]
+ initWithProfile:profile_]);
+ const SessionStartupPref startupPref =
+ SessionStartupPref::GetStartupPref(prefs_);
+ [customPagesSource_ setURLs:startupPref.urls];
+
+ // Set up the model for the default search popup. Register for notifications
+ // about when the model changes so we can update the selection in the view.
+ searchEngineModel_.reset(
+ [[SearchEngineListModel alloc]
+ initWithModel:profile->GetTemplateURLModel()]);
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(searchEngineModelChanged:)
+ name:kSearchEngineListModelChangedNotification
+ object:searchEngineModel_.get()];
+
+ // This needs to be done before awakeFromNib: because the bindings set up
+ // in the nib rely on it.
+ [self registerPrefObservers];
+
+ // Use one animation so we can stop it if the user clicks quickly, and
+ // start the new animation.
+ animation_.reset([[NSViewAnimation alloc] init]);
+ // Make this the delegate so it can remove the old view at the end of the
+ // animation (once it is faded out).
+ [animation_ setDelegate:self];
+ [animation_ setAnimationBlockingMode:NSAnimationNonblocking];
+
+ // TODO(akalin): handle incognito profiles? The windows version of this
+ // (in chrome/browser/views/options/content_page_view.cc) just does what
+ // we do below.
+ syncService_ = profile_->GetProfileSyncService();
+
+ // TODO(akalin): This color is taken from kSyncLabelErrorBgColor in
+ // content_page_view.cc. Either decomp that color out into a
+ // function/variable that is referenced by both this file and
+ // content_page_view.cc, or maybe pick a more suitable color.
+ syncErrorBackgroundColor_.reset(
+ [[NSColor colorWithDeviceRed:0xff/255.0
+ green:0x9a/255.0
+ blue:0x9a/255.0
+ alpha:1.0] retain]);
+
+ // Disable the |autoFillSettingsButton_| if we have no
+ // |personalDataManager|.
+ PersonalDataManager* personalDataManager =
+ profile_->GetPersonalDataManager();
+ [autoFillSettingsButton_ setHidden:(personalDataManager == NULL)];
+ bool autofill_disabled_by_policy =
+ autoFillEnabled_.IsManaged() && !autoFillEnabled_.GetValue();
+ [self setAutoFillSettingsButtonEnabled:!autofill_disabled_by_policy];
+ [self setPasswordManagerChoiceEnabled:!askSavePasswords_.IsManaged()];
+ [self setPasswordManagerButtonEnabled:
+ !askSavePasswords_.IsManaged() || askSavePasswords_.GetValue()];
+
+ // Initialize the enabled state of the elements on the general tab.
+ [self setShowHomeButtonEnabled:!showHomeButton_.IsManaged()];
+ [self setEnabledStateOfRestoreOnStartup];
+ [self setDefaultSearchEngineEnabled:![searchEngineModel_ isDefaultManaged]];
+
+ // Initialize UI state for the advanced page.
+ [self setShowAlternateErrorPagesEnabled:!alternateErrorPages_.IsManaged()];
+ [self setUseSuggestEnabled:!useSuggest_.IsManaged()];
+ [self setDnsPrefetchEnabled:!dnsPrefetch_.IsManaged()];
+ [self setSafeBrowsingEnabled:!safeBrowsing_.IsManaged()];
+ [self setMetricsReportingEnabled:!metricsReporting_.IsManaged()];
+ proxyPrefs_.reset(
+ PrefSetObserver::CreateProxyPrefSetObserver(prefs_, observer_.get()));
+ [self setProxiesConfigureButtonEnabled:!proxyPrefs_->IsManaged()];
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+
+ // Validate some assumptions in debug builds.
+
+ // "Basics", "Personal Stuff", and "Under the Hood" views should be the same
+ // width. They should be the same width so they are laid out to look as good
+ // as possible at that width with controls just having to wrap if their text
+ // is too long.
+ DCHECK_EQ(NSWidth([basicsView_ frame]), NSWidth([personalStuffView_ frame]))
+ << "Basics and Personal Stuff should be the same widths";
+ DCHECK_EQ(NSWidth([basicsView_ frame]), NSWidth([underTheHoodView_ frame]))
+ << "Basics and Under the Hood should be the same widths";
+ // "Under the Hood" content should always be skinnier than the scroller it
+ // goes into (we resize it).
+ DCHECK_LE(NSWidth([underTheHoodContentView_ frame]),
+ [underTheHoodScroller_ contentSize].width)
+ << "The Under the Hood content should be narrower than the content "
+ "of the scroller it goes into";
+
+#if !defined(GOOGLE_CHROME_BUILD)
+ // "Enable logging" (breakpad and stats) is only in Google Chrome builds,
+ // remove the checkbox and slide everything above it down.
+ RemoveViewFromView(underTheHoodContentView_, enableLoggingCheckbox_);
+#endif // !defined(GOOGLE_CHROME_BUILD)
+
+ // There are four problem children within the groups:
+ // Basics - Default Browser
+ // Personal Stuff - Sync
+ // Personal Stuff - Themes
+ // Personal Stuff - Browser Data
+ // These four have buttons that with some localizations are wider then the
+ // view. So the four get manually laid out before doing the general work so
+ // the views/window can be made wide enough to fit them. The layout in the
+ // general pass is a noop for these buttons (since they are already sized).
+
+ // Size the default browser button.
+ const NSUInteger kDefaultBrowserGroupCount = 3;
+ const NSUInteger kDefaultBrowserButtonIndex = 1;
+ DCHECK_EQ([basicsGroupDefaultBrowser_ count], kDefaultBrowserGroupCount)
+ << "Expected only two items in Default Browser group";
+ NSButton* defaultBrowserButton =
+ [basicsGroupDefaultBrowser_ objectAtIndex:kDefaultBrowserButtonIndex];
+ NSSize defaultBrowserChange =
+ [GTMUILocalizerAndLayoutTweaker sizeToFitView:defaultBrowserButton];
+ DCHECK_EQ(defaultBrowserChange.height, 0.0)
+ << "Button should have been right height in nib";
+
+ [self configureInstant];
+
+ // Size the sync row.
+ CGFloat syncRowChange = SizeToFitButtonPair(syncButton_,
+ syncCustomizeButton_);
+
+ // Size the themes row.
+ const NSUInteger kThemeGroupCount = 3;
+ const NSUInteger kThemeResetButtonIndex = 1;
+ const NSUInteger kThemeThemesButtonIndex = 2;
+ DCHECK_EQ([personalStuffGroupThemes_ count], kThemeGroupCount)
+ << "Expected only two items in Themes group";
+ CGFloat themeRowChange = SizeToFitButtonPair(
+ [personalStuffGroupThemes_ objectAtIndex:kThemeResetButtonIndex],
+ [personalStuffGroupThemes_ objectAtIndex:kThemeThemesButtonIndex]);
+
+ // Size the Privacy and Clear buttons that make a row in Under the Hood.
+ CGFloat privacyRowChange = SizeToFitButtonPair(contentSettingsButton_,
+ clearDataButton_);
+ // Under the Hood view is narrower (then the other panes) in the nib, subtract
+ // out the amount it was already going to grow to match the other panes when
+ // calculating how much the row needs things to grow.
+ privacyRowChange -=
+ ([underTheHoodScroller_ contentSize].width -
+ NSWidth([underTheHoodContentView_ frame]));
+
+ // Find the most any row changed in size.
+ CGFloat maxWidthChange = std::max(defaultBrowserChange.width, syncRowChange);
+ maxWidthChange = std::max(maxWidthChange, themeRowChange);
+ maxWidthChange = std::max(maxWidthChange, privacyRowChange);
+
+ // If any grew wider, make the views wider. If they all shrank, they fit the
+ // existing view widths, so no change is needed//.
+ if (maxWidthChange > 0.0) {
+ NSSize viewSize = [basicsView_ frame].size;
+ viewSize.width += maxWidthChange;
+ [basicsView_ setFrameSize:viewSize];
+ viewSize = [personalStuffView_ frame].size;
+ viewSize.width += maxWidthChange;
+ [personalStuffView_ setFrameSize:viewSize];
+ }
+
+ // Now that we have the width needed for Basics and Personal Stuff, lay out
+ // those pages bottom up making sure the strings fit and moving things up as
+ // needed.
+
+ CGFloat newWidth = NSWidth([basicsView_ frame]);
+ CGFloat verticalShift = 0.0;
+ verticalShift += AutoSizeGroup(basicsGroupDefaultBrowser_,
+ kAutoSizeGroupBehaviorVerticalFirstToFit,
+ verticalShift);
+ // TODO(rsesek/rohitrao): This is ugly, when the instant experiement is no
+ // longer displayed, please remove this code, the NSTextField and IBOutlet
+ // needed.
+ DCHECK(instantExperiment_ != nil);
+ if (verticalShift) {
+ // If the default browser moved things up, move the experiment field up
+ // also, it is not in the SearchEngine group due to its position on screen.
+ NSPoint origin = [instantExperiment_ frame].origin;
+ origin.y += verticalShift;
+ [instantExperiment_ setFrameOrigin:origin];
+ }
+ // End TODO
+ verticalShift += AutoSizeGroup(basicsGroupSearchEngine_,
+ kAutoSizeGroupBehaviorFirstTwoAsRowVerticalToFit,
+ verticalShift);
+ verticalShift += AutoSizeGroup(basicsGroupToolbar_,
+ kAutoSizeGroupBehaviorVerticalToFit,
+ verticalShift);
+ verticalShift += AutoSizeGroup(basicsGroupHomePage_,
+ kAutoSizeGroupBehaviorVerticalToFit,
+ verticalShift);
+ verticalShift += AutoSizeGroup(basicsGroupStartup_,
+ kAutoSizeGroupBehaviorVerticalFirstToFit,
+ verticalShift);
+ [GTMUILocalizerAndLayoutTweaker
+ resizeViewWithoutAutoResizingSubViews:basicsView_
+ delta:NSMakeSize(0.0, verticalShift)];
+
+ verticalShift = 0.0;
+ verticalShift += AutoSizeGroup(personalStuffGroupThemes_,
+ kAutoSizeGroupBehaviorHorizontalToFit,
+ verticalShift);
+ verticalShift += AutoSizeGroup(personalStuffGroupBrowserData_,
+ kAutoSizeGroupBehaviorVerticalToFit,
+ verticalShift);
+ verticalShift += AutoSizeGroup(personalStuffGroupAutofill_,
+ kAutoSizeGroupBehaviorVerticalToFit,
+ verticalShift);
+ verticalShift += AutoSizeGroup(personalStuffGroupPasswords_,
+ kAutoSizeGroupBehaviorVerticalToFit,
+ verticalShift);
+ // TODO(akalin): Here we rely on the initial contents of the sync
+ // group's text field/link field to be large enough to hold all
+ // possible messages so that we don't have to re-layout when sync
+ // state changes. This isn't perfect, since e.g. some sync messages
+ // use the user's e-mail address (which may be really long), and the
+ // link field is usually not shown (leaving a big empty space).
+ // Rethink sync preferences UI for Mac.
+ verticalShift += AutoSizeGroup(personalStuffGroupSync_,
+ kAutoSizeGroupBehaviorVerticalToFit,
+ verticalShift);
+ [GTMUILocalizerAndLayoutTweaker
+ resizeViewWithoutAutoResizingSubViews:personalStuffView_
+ delta:NSMakeSize(0.0, verticalShift)];
+
+ if (syncService_) {
+ syncService_->AddObserver(observer_.get());
+ // Update the controls according to the initial state.
+ [self syncStateChanged];
+ } else {
+ // If sync is disabled we don't want to show the sync controls at all.
+ RemoveGroupFromView(personalStuffView_, personalStuffGroupSync_);
+ }
+
+ // Make the window as wide as the views.
+ NSWindow* prefsWindow = [self window];
+ NSView* prefsContentView = [prefsWindow contentView];
+ NSRect frame = [prefsContentView convertRect:[prefsWindow frame]
+ fromView:nil];
+ frame.size.width = newWidth;
+ frame = [prefsContentView convertRect:frame toView:nil];
+ [prefsWindow setFrame:frame display:NO];
+
+ // The Under the Hood prefs is a scroller, it shouldn't get any border, so it
+ // gets resized to be as wide as the window ended up.
+ NSSize underTheHoodSize = [underTheHoodView_ frame].size;
+ underTheHoodSize.width = newWidth;
+ [underTheHoodView_ setFrameSize:underTheHoodSize];
+
+ // Widen the Under the Hood content so things can rewrap to the full width.
+ NSSize underTheHoodContentSize = [underTheHoodContentView_ frame].size;
+ underTheHoodContentSize.width = [underTheHoodScroller_ contentSize].width;
+ [underTheHoodContentView_ setFrameSize:underTheHoodContentSize];
+
+ // Now that Under the Hood is the right width, auto-size to the new width to
+ // get the final height.
+ verticalShift = AutoSizeUnderTheHoodContent(underTheHoodContentView_,
+ downloadLocationControl_,
+ downloadLocationButton_);
+ [GTMUILocalizerAndLayoutTweaker
+ resizeViewWithoutAutoResizingSubViews:underTheHoodContentView_
+ delta:NSMakeSize(0.0, verticalShift)];
+ underTheHoodContentSize = [underTheHoodContentView_ frame].size;
+
+ // Put the Under the Hood content view into the scroller and scroll it to the
+ // top.
+ [underTheHoodScroller_ setDocumentView:underTheHoodContentView_];
+ [underTheHoodContentView_ scrollPoint:
+ NSMakePoint(0, underTheHoodContentSize.height)];
+
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ NSImage* alertIcon = rb.GetNativeImageNamed(IDR_WARNING);
+ DCHECK(alertIcon);
+ [managedPrefsBannerWarningImage_ setImage:alertIcon];
+
+ [self initBannerStateForPage:initialPage_];
+ [self switchToPage:initialPage_ animate:NO];
+
+ // Save/restore position based on prefs.
+ if (g_browser_process && g_browser_process->local_state()) {
+ sizeSaver_.reset([[WindowSizeAutosaver alloc]
+ initWithWindow:[self window]
+ prefService:g_browser_process->local_state()
+ path:prefs::kPreferencesWindowPlacement]);
+ }
+
+ // Initialize the banner gradient and stroke color.
+ NSColor* bannerStartingColor =
+ [NSColor colorWithCalibratedRed:kBannerGradientColorTop[0]
+ green:kBannerGradientColorTop[1]
+ blue:kBannerGradientColorTop[2]
+ alpha:1.0];
+ NSColor* bannerEndingColor =
+ [NSColor colorWithCalibratedRed:kBannerGradientColorBottom[0]
+ green:kBannerGradientColorBottom[1]
+ blue:kBannerGradientColorBottom[2]
+ alpha:1.0];
+ scoped_nsobject<NSGradient> bannerGradient(
+ [[NSGradient alloc] initWithStartingColor:bannerStartingColor
+ endingColor:bannerEndingColor]);
+ [managedPrefsBannerView_ setGradient:bannerGradient];
+
+ NSColor* bannerStrokeColor =
+ [NSColor colorWithCalibratedWhite:kBannerStrokeColor
+ alpha:1.0];
+ [managedPrefsBannerView_ setStrokeColor:bannerStrokeColor];
+
+ // Set accessibility related attributes.
+ NSTableView* tableView = [basicsView_ viewWithTag:kBasicsStartupPageTableTag];
+ NSString* description =
+ l10n_util::GetNSStringWithFixup(IDS_OPTIONS_STARTUP_SHOW_PAGES);
+ [tableView accessibilitySetOverrideValue:description
+ forAttribute:NSAccessibilityDescriptionAttribute];
+}
+
+- (void)dealloc {
+ if (syncService_) {
+ syncService_->RemoveObserver(observer_.get());
+ }
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [animation_ setDelegate:nil];
+ [animation_ stopAnimation];
+ [super dealloc];
+}
+
+// Xcode 3.1.x version of Interface Builder doesn't do a lot for editing
+// toolbars in XIB. So the toolbar's delegate is set to the controller so it
+// can tell the toolbar what items are selectable.
+- (NSArray*)toolbarSelectableItemIdentifiers:(NSToolbar*)toolbar {
+ DCHECK(toolbar == toolbar_);
+ return [[toolbar_ items] valueForKey:@"itemIdentifier"];
+}
+
+// Register our interest in the preferences we're displaying so if anything
+// else in the UI changes them we will be updated.
+- (void)registerPrefObservers {
+ if (!prefs_) return;
+
+ // Basics panel
+ registrar_.Init(prefs_);
+ registrar_.Add(prefs::kURLsToRestoreOnStartup, observer_.get());
+ restoreOnStartup_.Init(prefs::kRestoreOnStartup, prefs_, observer_.get());
+ newTabPageIsHomePage_.Init(prefs::kHomePageIsNewTabPage,
+ prefs_, observer_.get());
+ homepage_.Init(prefs::kHomePage, prefs_, observer_.get());
+ showHomeButton_.Init(prefs::kShowHomeButton, prefs_, observer_.get());
+ instantEnabled_.Init(prefs::kInstantEnabled, prefs_, observer_.get());
+
+ // Personal Stuff panel
+ askSavePasswords_.Init(prefs::kPasswordManagerEnabled,
+ prefs_, observer_.get());
+ autoFillEnabled_.Init(prefs::kAutoFillEnabled, prefs_, observer_.get());
+ currentTheme_.Init(prefs::kCurrentThemeID, prefs_, observer_.get());
+
+ // Under the hood panel
+ alternateErrorPages_.Init(prefs::kAlternateErrorPagesEnabled,
+ prefs_, observer_.get());
+ useSuggest_.Init(prefs::kSearchSuggestEnabled, prefs_, observer_.get());
+ dnsPrefetch_.Init(prefs::kDnsPrefetchingEnabled, prefs_, observer_.get());
+ safeBrowsing_.Init(prefs::kSafeBrowsingEnabled, prefs_, observer_.get());
+ autoOpenFiles_.Init(
+ prefs::kDownloadExtensionsToOpen, prefs_, observer_.get());
+ translateEnabled_.Init(prefs::kEnableTranslate, prefs_, observer_.get());
+ tabsToLinks_.Init(prefs::kWebkitTabsToLinks, prefs_, observer_.get());
+
+ // During unit tests, there is no local state object, so we fall back to
+ // the prefs object (where we've explicitly registered this pref so we
+ // know it's there).
+ PrefService* local = g_browser_process->local_state();
+ if (!local)
+ local = prefs_;
+ metricsReporting_.Init(prefs::kMetricsReportingEnabled,
+ local, observer_.get());
+ defaultDownloadLocation_.Init(prefs::kDownloadDefaultDirectory, prefs_,
+ observer_.get());
+ askForSaveLocation_.Init(prefs::kPromptForDownload, prefs_, observer_.get());
+
+ // We don't need to observe changes in this value.
+ lastSelectedPage_.Init(prefs::kOptionsWindowLastTabIndex, local, NULL);
+}
+
+// Called when the window wants to be closed.
+- (BOOL)windowShouldClose:(id)sender {
+ // Stop any animation and clear the delegate to avoid stale pointers.
+ [animation_ setDelegate:nil];
+ [animation_ stopAnimation];
+
+ return YES;
+}
+
+// Called when the user hits the escape key. Closes the window.
+- (void)cancel:(id)sender {
+ [[self window] performClose:self];
+}
+
+// Record the user performed a certain action and save the preferences.
+- (void)recordUserAction:(const UserMetricsAction &)action {
+ UserMetrics::RecordAction(action, profile_);
+ if (prefs_)
+ prefs_->ScheduleSavePersistentPrefs();
+}
+
+// Returns the set of keys that |key| depends on for its value so it can be
+// re-computed when any of those change as well.
++ (NSSet*)keyPathsForValuesAffectingValueForKey:(NSString*)key {
+ NSSet* paths = [super keyPathsForValuesAffectingValueForKey:key];
+ if ([key isEqualToString:@"isHomepageURLEnabled"]) {
+ paths = [paths setByAddingObject:@"newTabPageIsHomePageIndex"];
+ paths = [paths setByAddingObject:@"homepageURL"];
+ } else if ([key isEqualToString:@"restoreURLsEnabled"]) {
+ paths = [paths setByAddingObject:@"restoreOnStartupIndex"];
+ } else if ([key isEqualToString:@"isHomepageChoiceEnabled"]) {
+ paths = [paths setByAddingObject:@"newTabPageIsHomePageIndex"];
+ paths = [paths setByAddingObject:@"homepageURL"];
+ } else if ([key isEqualToString:@"newTabPageIsHomePageIndex"]) {
+ paths = [paths setByAddingObject:@"homepageURL"];
+ } else if ([key isEqualToString:@"hompageURL"]) {
+ paths = [paths setByAddingObject:@"newTabPageIsHomePageIndex"];
+ } else if ([key isEqualToString:@"isDefaultBrowser"]) {
+ paths = [paths setByAddingObject:@"defaultBrowser"];
+ } else if ([key isEqualToString:@"defaultBrowserTextColor"]) {
+ paths = [paths setByAddingObject:@"defaultBrowser"];
+ } else if ([key isEqualToString:@"defaultBrowserText"]) {
+ paths = [paths setByAddingObject:@"defaultBrowser"];
+ }
+ return paths;
+}
+
+// Launch the Keychain Access app.
+- (void)launchKeychainAccess {
+ NSString* const kKeychainBundleId = @"com.apple.keychainaccess";
+ [[NSWorkspace sharedWorkspace]
+ launchAppWithBundleIdentifier:kKeychainBundleId
+ options:0L
+ additionalEventParamDescriptor:nil
+ launchIdentifier:nil];
+}
+
+//-------------------------------------------------------------------------
+// Basics panel
+
+// Sets the home page preferences for kNewTabPageIsHomePage and kHomePage. If a
+// blank or null-host URL is passed in we revert to using NewTab page
+// as the Home page. Note: using SetValue() causes the observers not to fire,
+// which is actually a good thing as we could end up in a state where setting
+// the homepage to an empty url would automatically reset the prefs back to
+// using the NTP, so we'd be never be able to change it.
+- (void)setHomepage:(const GURL&)homepage {
+ if (IsNewTabUIURLString(homepage)) {
+ newTabPageIsHomePage_.SetValueIfNotManaged(true);
+ homepage_.SetValueIfNotManaged(std::string());
+ } else if (!homepage.is_valid()) {
+ newTabPageIsHomePage_.SetValueIfNotManaged(true);
+ if (!homepage.has_host())
+ homepage_.SetValueIfNotManaged(std::string());
+ } else {
+ homepage_.SetValueIfNotManaged(homepage.spec());
+ }
+}
+
+// Callback when preferences are changed by someone modifying the prefs backend
+// externally. |prefName| is the name of the pref that has changed. Unlike on
+// Windows, we don't need to use this method for initializing, that's handled by
+// Cocoa Bindings.
+// Handles prefs for the "Basics" panel.
+- (void)basicsPrefChanged:(std::string*)prefName {
+ if (*prefName == prefs::kRestoreOnStartup) {
+ const SessionStartupPref startupPref =
+ SessionStartupPref::GetStartupPref(prefs_);
+ [self setRestoreOnStartupIndex:startupPref.type];
+ [self setEnabledStateOfRestoreOnStartup];
+ } else if (*prefName == prefs::kURLsToRestoreOnStartup) {
+ [customPagesSource_ reloadURLs];
+ [self setEnabledStateOfRestoreOnStartup];
+ } else if (*prefName == prefs::kHomePageIsNewTabPage) {
+ NSInteger useNewTabPage = newTabPageIsHomePage_.GetValue() ? 0 : 1;
+ [self setNewTabPageIsHomePageIndex:useNewTabPage];
+ } else if (*prefName == prefs::kHomePage) {
+ NSString* value = base::SysUTF8ToNSString(homepage_.GetValue());
+ [self setHomepageURL:value];
+ } else if (*prefName == prefs::kShowHomeButton) {
+ [self setShowHomeButton:showHomeButton_.GetValue() ? YES : NO];
+ [self setShowHomeButtonEnabled:!showHomeButton_.IsManaged()];
+ } else if (*prefName == prefs::kInstantEnabled) {
+ [self configureInstant];
+ }
+}
+
+// Returns the index of the selected cell in the "on startup" matrix based
+// on the "restore on startup" pref. The ordering of the cells is in the
+// same order as the pref.
+- (NSInteger)restoreOnStartupIndex {
+ const SessionStartupPref pref = SessionStartupPref::GetStartupPref(prefs_);
+ return pref.type;
+}
+
+// A helper function that takes the startup session type, grabs the URLs to
+// restore, and saves it all in prefs.
+- (void)saveSessionStartupWithType:(SessionStartupPref::Type)type {
+ SessionStartupPref pref;
+ pref.type = type;
+ pref.urls = [customPagesSource_.get() URLs];
+ SessionStartupPref::SetStartupPref(prefs_, pref);
+}
+
+// Sets the pref based on the index of the selected cell in the matrix and
+// marks the appropriate user metric.
+- (void)setRestoreOnStartupIndex:(NSInteger)type {
+ SessionStartupPref::Type startupType =
+ static_cast<SessionStartupPref::Type>(type);
+ switch (startupType) {
+ case SessionStartupPref::DEFAULT:
+ [self recordUserAction:UserMetricsAction("Options_Startup_Homepage")];
+ break;
+ case SessionStartupPref::LAST:
+ [self recordUserAction:UserMetricsAction("Options_Startup_LastSession")];
+ break;
+ case SessionStartupPref::URLS:
+ [self recordUserAction:UserMetricsAction("Options_Startup_Custom")];
+ break;
+ default:
+ NOTREACHED();
+ }
+ [self saveSessionStartupWithType:startupType];
+}
+
+// Enables or disables the restoreOnStartup elements
+- (void) setEnabledStateOfRestoreOnStartup {
+ const SessionStartupPref startupPref =
+ SessionStartupPref::GetStartupPref(prefs_);
+ [self setRestoreButtonsEnabled:!SessionStartupPref::TypeIsManaged(prefs_)];
+ [self setRestoreURLsEnabled:!SessionStartupPref::URLsAreManaged(prefs_) &&
+ [self restoreOnStartupIndex] == SessionStartupPref::URLS];
+}
+
+// Getter for the |customPagesSource| property for bindings.
+- (CustomHomePagesModel*)customPagesSource {
+ return customPagesSource_.get();
+}
+
+// Called when the selection in the table changes. If a flag is set indicating
+// that we're waiting for a special select message, edit the cell. Otherwise
+// just ignore it, we don't normally care.
+- (void)tableViewSelectionDidChange:(NSNotification*)aNotification {
+ if (pendingSelectForEdit_) {
+ NSTableView* table = [aNotification object];
+ NSUInteger selectedRow = [table selectedRow];
+ [table editColumn:0 row:selectedRow withEvent:nil select:YES];
+ pendingSelectForEdit_ = NO;
+ }
+}
+
+// Called when the user hits the (+) button for adding a new homepage to the
+// list. This will also attempt to make the new item editable so the user can
+// just start typing.
+- (IBAction)addHomepage:(id)sender {
+ [customPagesArrayController_ add:sender];
+
+ // When the new item is added to the model, the array controller will select
+ // it. We'll watch for that notification (because we are the table view's
+ // delegate) and then make the cell editable. Note that this can't be
+ // accomplished simply by subclassing the array controller's add method (I
+ // did try). The update of the table is asynchronous with the controller
+ // updating the model.
+ pendingSelectForEdit_ = YES;
+}
+
+// Called when the user hits the (-) button for removing the selected items in
+// the homepage table. The controller does all the work.
+- (IBAction)removeSelectedHomepages:(id)sender {
+ [customPagesArrayController_ remove:sender];
+}
+
+// Add all entries for all open browsers with our profile.
+- (IBAction)useCurrentPagesAsHomepage:(id)sender {
+ std::vector<GURL> urls;
+ for (BrowserList::const_iterator browserIter = BrowserList::begin();
+ browserIter != BrowserList::end(); ++browserIter) {
+ Browser* browser = *browserIter;
+ if (browser->profile() != profile_)
+ continue; // Only want entries for open profile.
+
+ for (int tabIndex = 0; tabIndex < browser->tab_count(); ++tabIndex) {
+ TabContents* tab = browser->GetTabContentsAt(tabIndex);
+ if (tab->ShouldDisplayURL()) {
+ const GURL url = browser->GetTabContentsAt(tabIndex)->GetURL();
+ if (!url.is_empty())
+ urls.push_back(url);
+ }
+ }
+ }
+ [customPagesSource_ setURLs:urls];
+}
+
+enum { kHomepageNewTabPage, kHomepageURL };
+
+// Here's a table describing the desired characteristics of the homepage choice
+// radio value, it's enabled state and the URL field enabled state. They depend
+// on the values of the managed bits for homepage (m_hp) and
+// homepageIsNewTabPage (m_ntp) preferences, as well as the value of the
+// homepageIsNewTabPage preference (ntp) and whether the homepage preference
+// is equal to the new tab page URL (hpisntp).
+//
+// m_hp m_ntp ntp hpisntp | choice value | choice enabled | URL field enabled
+// --------------------------------------------------------------------------
+// 0 0 0 0 | homepage | 1 | 1
+// 0 0 0 1 | new tab page | 1 | 0
+// 0 0 1 0 | new tab page | 1 | 0
+// 0 0 1 1 | new tab page | 1 | 0
+// 0 1 0 0 | homepage | 0 | 1
+// 0 1 0 1 | homepage | 0 | 1
+// 0 1 1 0 | new tab page | 0 | 0
+// 0 1 1 1 | new tab page | 0 | 0
+// 1 0 0 0 | homepage | 1 | 0
+// 1 0 0 1 | new tab page | 0 | 0
+// 1 0 1 0 | new tab page | 1 | 0
+// 1 0 1 1 | new tab page | 0 | 0
+// 1 1 0 0 | homepage | 0 | 0
+// 1 1 0 1 | new tab page | 0 | 0
+// 1 1 1 0 | new tab page | 0 | 0
+// 1 1 1 1 | new tab page | 0 | 0
+//
+// thus, we have:
+//
+// choice value is new tab page === ntp || (hpisntp && (m_hp || !m_ntp))
+// choice enabled === !m_ntp && !(m_hp && hpisntp)
+// URL field enabled === !ntp && !mhp && !(hpisntp && !m_ntp)
+//
+// which also make sense if you think about them.
+
+// Checks whether the homepage URL refers to the new tab page.
+- (BOOL)isHomepageNewTabUIURL {
+ return IsNewTabUIURLString(GURL(homepage_.GetValue().c_str()));
+}
+
+// Returns the index of the selected cell in the "home page" marix based on
+// the "new tab is home page" pref. Sadly, the ordering is reversed from the
+// pref value.
+- (NSInteger)newTabPageIsHomePageIndex {
+ return newTabPageIsHomePage_.GetValue() ||
+ ([self isHomepageNewTabUIURL] &&
+ (homepage_.IsManaged() || !newTabPageIsHomePage_.IsManaged())) ?
+ kHomepageNewTabPage : kHomepageURL;
+}
+
+// Sets the pref based on the given index into the matrix and marks the
+// appropriate user metric.
+- (void)setNewTabPageIsHomePageIndex:(NSInteger)index {
+ bool useNewTabPage = index == kHomepageNewTabPage ? true : false;
+ if (useNewTabPage) {
+ [self recordUserAction:UserMetricsAction("Options_Homepage_UseNewTab")];
+ } else {
+ [self recordUserAction:UserMetricsAction("Options_Homepage_UseURL")];
+ if ([self isHomepageNewTabUIURL])
+ homepage_.SetValueIfNotManaged(std::string());
+ }
+ newTabPageIsHomePage_.SetValueIfNotManaged(useNewTabPage);
+}
+
+// Check whether the new tab and URL homepage radios should be enabled, i.e. if
+// the corresponding preference is not managed through configuration policy.
+- (BOOL)isHomepageChoiceEnabled {
+ return !newTabPageIsHomePage_.IsManaged() &&
+ !(homepage_.IsManaged() && [self isHomepageNewTabUIURL]);
+}
+
+// Returns whether or not the homepage URL text field should be enabled
+// based on if the new tab page is the home page.
+- (BOOL)isHomepageURLEnabled {
+ return !newTabPageIsHomePage_.GetValue() && !homepage_.IsManaged() &&
+ !([self isHomepageNewTabUIURL] && !newTabPageIsHomePage_.IsManaged());
+}
+
+// Returns the homepage URL.
+- (NSString*)homepageURL {
+ NSString* value = base::SysUTF8ToNSString(homepage_.GetValue());
+ return [self isHomepageNewTabUIURL] ? nil : value;
+}
+
+// Sets the homepage URL to |urlString| with some fixing up.
+- (void)setHomepageURL:(NSString*)urlString {
+ // If the text field contains a valid URL, sync it to prefs. We run it
+ // through the fixer upper to allow input like "google.com" to be converted
+ // to something valid ("http://google.com").
+ std::string unfixedURL = urlString ? base::SysNSStringToUTF8(urlString) :
+ chrome::kChromeUINewTabURL;
+ [self setHomepage:URLFixerUpper::FixupURL(unfixedURL, std::string())];
+}
+
+// Returns whether the home button should be checked based on the preference.
+- (BOOL)showHomeButton {
+ return showHomeButton_.GetValue() ? YES : NO;
+}
+
+// Sets the backend pref for whether or not the home button should be displayed
+// based on |value|.
+- (void)setShowHomeButton:(BOOL)value {
+ if (value)
+ [self recordUserAction:UserMetricsAction(
+ "Options_Homepage_ShowHomeButton")];
+ else
+ [self recordUserAction:UserMetricsAction(
+ "Options_Homepage_HideHomeButton")];
+ showHomeButton_.SetValueIfNotManaged(value ? true : false);
+}
+
+// Getter for the |searchEngineModel| property for bindings.
+- (id)searchEngineModel {
+ return searchEngineModel_.get();
+}
+
+// Bindings for the search engine popup. We not binding directly to the model
+// in order to siphon off the setter so we can record the metric. If we're
+// doing it with one, might as well do it with both.
+- (NSUInteger)searchEngineSelectedIndex {
+ return [searchEngineModel_ defaultIndex];
+}
+
+- (void)setSearchEngineSelectedIndex:(NSUInteger)index {
+ [self recordUserAction:UserMetricsAction("Options_SearchEngineChanged")];
+ [searchEngineModel_ setDefaultIndex:index];
+}
+
+// Called when the search engine model changes. Update the selection in the
+// popup by tickling the bindings with the new value.
+- (void)searchEngineModelChanged:(NSNotification*)notify {
+ [self setSearchEngineSelectedIndex:[self searchEngineSelectedIndex]];
+ [self setDefaultSearchEngineEnabled:![searchEngineModel_ isDefaultManaged]];
+
+}
+
+- (IBAction)manageSearchEngines:(id)sender {
+ [KeywordEditorCocoaController showKeywordEditor:profile_];
+}
+
+- (IBAction)toggleInstant:(id)sender {
+ if (instantEnabled_.GetValue()) {
+ InstantController::Disable(profile_);
+ } else {
+ [instantCheckbox_ setState:NSOffState];
+ browser::ShowInstantConfirmDialogIfNecessary([self window], profile_);
+ }
+}
+
+// Sets the state of the Instant checkbox and adds the type information to the
+// label.
+- (void)configureInstant {
+ bool enabled = instantEnabled_.GetValue();
+ NSInteger state = enabled ? NSOnState : NSOffState;
+ [instantCheckbox_ setState:state];
+
+ [instantExperiment_ setStringValue:@""];
+}
+
+- (IBAction)learnMoreAboutInstant:(id)sender {
+ browser::ShowOptionsURL(profile_, GURL(browser::kInstantLearnMoreURL));
+}
+
+// Called when the user clicks the button to make Chromium the default
+// browser. Registers http and https.
+- (IBAction)makeDefaultBrowser:(id)sender {
+ [self willChangeValueForKey:@"defaultBrowser"];
+
+ ShellIntegration::SetAsDefaultBrowser();
+ [self recordUserAction:UserMetricsAction("Options_SetAsDefaultBrowser")];
+ // If the user made Chrome the default browser, then he/she arguably wants
+ // to be notified when that changes.
+ prefs_->SetBoolean(prefs::kCheckDefaultBrowser, true);
+
+ // Tickle KVO so that the UI updates.
+ [self didChangeValueForKey:@"defaultBrowser"];
+}
+
+// Returns the Chromium default browser state.
+- (ShellIntegration::DefaultBrowserState)isDefaultBrowser {
+ return ShellIntegration::IsDefaultBrowser();
+}
+
+// Returns the text color of the "chromium is your default browser" text (green
+// for yes, red for no).
+- (NSColor*)defaultBrowserTextColor {
+ ShellIntegration::DefaultBrowserState state = [self isDefaultBrowser];
+ return (state == ShellIntegration::IS_DEFAULT_BROWSER) ?
+ [NSColor colorWithCalibratedRed:0.0 green:135.0/255.0 blue:0 alpha:1.0] :
+ [NSColor colorWithCalibratedRed:135.0/255.0 green:0 blue:0 alpha:1.0];
+}
+
+// Returns the text for the "chromium is your default browser" string dependent
+// on if Chromium actually is or not.
+- (NSString*)defaultBrowserText {
+ ShellIntegration::DefaultBrowserState state = [self isDefaultBrowser];
+ int stringId;
+ if (state == ShellIntegration::IS_DEFAULT_BROWSER)
+ stringId = IDS_OPTIONS_DEFAULTBROWSER_DEFAULT;
+ else if (state == ShellIntegration::NOT_DEFAULT_BROWSER)
+ stringId = IDS_OPTIONS_DEFAULTBROWSER_NOTDEFAULT;
+ else
+ stringId = IDS_OPTIONS_DEFAULTBROWSER_UNKNOWN;
+ string16 text =
+ l10n_util::GetStringFUTF16(stringId,
+ l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
+ return base::SysUTF16ToNSString(text);
+}
+
+//-------------------------------------------------------------------------
+// User Data panel
+
+// Since passwords and forms are radio groups, 'enabled' is index 0 and
+// 'disabled' is index 1. Yay.
+const int kEnabledIndex = 0;
+const int kDisabledIndex = 1;
+
+// Callback when preferences are changed. |prefName| is the name of the pref
+// that has changed. Unlike on Windows, we don't need to use this method for
+// initializing, that's handled by Cocoa Bindings.
+// Handles prefs for the "Personal Stuff" panel.
+- (void)userDataPrefChanged:(std::string*)prefName {
+ if (*prefName == prefs::kPasswordManagerEnabled) {
+ [self setPasswordManagerEnabledIndex:askSavePasswords_.GetValue() ?
+ kEnabledIndex : kDisabledIndex];
+ [self setPasswordManagerChoiceEnabled:!askSavePasswords_.IsManaged()];
+ [self setPasswordManagerButtonEnabled:
+ !askSavePasswords_.IsManaged() || askSavePasswords_.GetValue()];
+ }
+ if (*prefName == prefs::kAutoFillEnabled) {
+ bool autofill_disabled_by_policy =
+ autoFillEnabled_.IsManaged() && !autoFillEnabled_.GetValue();
+ [self setAutoFillSettingsButtonEnabled:!autofill_disabled_by_policy];
+ }
+ if (*prefName == prefs::kCurrentThemeID) {
+ [self setIsUsingDefaultTheme:currentTheme_.GetValue().length() == 0];
+ }
+}
+
+// Called to launch the Keychain Access app to show the user's stored
+// passwords.
+- (IBAction)showSavedPasswords:(id)sender {
+ [self recordUserAction:UserMetricsAction("Options_ShowPasswordsExceptions")];
+ [self launchKeychainAccess];
+}
+
+// Called to show the Auto Fill Settings dialog.
+- (IBAction)showAutoFillSettings:(id)sender {
+ [self recordUserAction:UserMetricsAction("Options_ShowAutoFillSettings")];
+
+ PersonalDataManager* personalDataManager = profile_->GetPersonalDataManager();
+ if (!personalDataManager) {
+ // Should not reach here because button is disabled when
+ // |personalDataManager| is NULL.
+ NOTREACHED();
+ return;
+ }
+
+ ShowAutoFillDialog(NULL, personalDataManager, profile_);
+}
+
+// Called to import data from other browsers (Safari, Firefox, etc).
+- (IBAction)importData:(id)sender {
+ UserMetrics::RecordAction(UserMetricsAction("Import_ShowDlg"), profile_);
+ [ImportSettingsDialogController showImportSettingsDialogForProfile:profile_];
+}
+
+- (IBAction)resetThemeToDefault:(id)sender {
+ [self recordUserAction:UserMetricsAction("Options_ThemesReset")];
+ profile_->ClearTheme();
+}
+
+- (IBAction)themesGallery:(id)sender {
+ [self recordUserAction:UserMetricsAction("Options_ThemesGallery")];
+ Browser* browser = BrowserList::GetLastActive();
+
+ if (!browser || !browser->GetSelectedTabContents())
+ browser = Browser::Create(profile_);
+ browser->OpenThemeGalleryTabAndActivate();
+}
+
+// Called when the "stop syncing" confirmation dialog started by
+// doSyncAction is finished. Stop syncing only If the user clicked
+// OK.
+- (void)stopSyncAlertDidEnd:(NSAlert*)alert
+ returnCode:(int)returnCode
+ contextInfo:(void*)contextInfo {
+ DCHECK(syncService_ && !syncService_->IsManaged());
+ if (returnCode == NSAlertFirstButtonReturn) {
+ syncService_->DisableForUser();
+ ProfileSyncService::SyncEvent(ProfileSyncService::STOP_FROM_OPTIONS);
+ }
+}
+
+// Called when the user clicks the multi-purpose sync button in the
+// "Personal Stuff" pane.
+- (IBAction)doSyncAction:(id)sender {
+ DCHECK(syncService_ && !syncService_->IsManaged());
+ if (syncService_->HasSyncSetupCompleted()) {
+ // If sync setup has completed that means the sync button was a
+ // "stop syncing" button. Bring up a confirmation dialog before
+ // actually stopping syncing (see stopSyncAlertDidEnd).
+ scoped_nsobject<NSAlert> alert([[NSAlert alloc] init]);
+ [alert addButtonWithTitle:l10n_util::GetNSStringWithFixup(
+ IDS_SYNC_STOP_SYNCING_CONFIRM_BUTTON_LABEL)];
+ [alert addButtonWithTitle:l10n_util::GetNSStringWithFixup(
+ IDS_CANCEL)];
+ [alert setMessageText:l10n_util::GetNSStringWithFixup(
+ IDS_SYNC_STOP_SYNCING_DIALOG_TITLE)];
+ [alert setInformativeText:l10n_util::GetNSStringFWithFixup(
+ IDS_SYNC_STOP_SYNCING_EXPLANATION_LABEL,
+ l10n_util::GetStringUTF16(IDS_PRODUCT_NAME))];
+ [alert setAlertStyle:NSWarningAlertStyle];
+ const SEL kEndSelector =
+ @selector(stopSyncAlertDidEnd:returnCode:contextInfo:);
+ [alert beginSheetModalForWindow:[self window]
+ modalDelegate:self
+ didEndSelector:kEndSelector
+ contextInfo:NULL];
+ } else {
+ // Otherwise, the sync button was a "sync my bookmarks" button.
+ // Kick off the sync setup process.
+ syncService_->ShowLoginDialog(NULL);
+ ProfileSyncService::SyncEvent(ProfileSyncService::START_FROM_OPTIONS);
+ }
+}
+
+// Called when the user clicks on the link to the privacy dashboard.
+- (IBAction)showPrivacyDashboard:(id)sender {
+ Browser* browser = BrowserList::GetLastActive();
+
+ if (!browser || !browser->GetSelectedTabContents())
+ browser = Browser::Create(profile_);
+ browser->OpenPrivacyDashboardTabAndActivate();
+}
+
+// Called when the user clicks the "Customize Sync" button in the
+// "Personal Stuff" pane. Spawns a dialog-modal sheet that cleans
+// itself up on close.
+- (IBAction)doSyncCustomize:(id)sender {
+ syncService_->ShowConfigure(NULL);
+}
+
+- (IBAction)doSyncReauthentication:(id)sender {
+ DCHECK(syncService_ && !syncService_->IsManaged());
+ syncService_->ShowLoginDialog(NULL);
+}
+
+- (void)setPasswordManagerEnabledIndex:(NSInteger)value {
+ if (value == kEnabledIndex)
+ [self recordUserAction:UserMetricsAction(
+ "Options_PasswordManager_Enable")];
+ else
+ [self recordUserAction:UserMetricsAction(
+ "Options_PasswordManager_Disable")];
+ askSavePasswords_.SetValueIfNotManaged(value == kEnabledIndex ? true : false);
+}
+
+- (NSInteger)passwordManagerEnabledIndex {
+ return askSavePasswords_.GetValue() ? kEnabledIndex : kDisabledIndex;
+}
+
+- (void)setIsUsingDefaultTheme:(BOOL)value {
+ if (value)
+ [self recordUserAction:UserMetricsAction(
+ "Options_IsUsingDefaultTheme_Enable")];
+ else
+ [self recordUserAction:UserMetricsAction(
+ "Options_IsUsingDefaultTheme_Disable")];
+}
+
+- (BOOL)isUsingDefaultTheme {
+ return currentTheme_.GetValue().length() == 0;
+}
+
+//-------------------------------------------------------------------------
+// Under the hood panel
+
+// Callback when preferences are changed. |prefName| is the name of the pref
+// that has changed. Unlike on Windows, we don't need to use this method for
+// initializing, that's handled by Cocoa Bindings.
+// Handles prefs for the "Under the hood" panel.
+- (void)underHoodPrefChanged:(std::string*)prefName {
+ if (*prefName == prefs::kAlternateErrorPagesEnabled) {
+ [self setShowAlternateErrorPages:
+ alternateErrorPages_.GetValue() ? YES : NO];
+ [self setShowAlternateErrorPagesEnabled:!alternateErrorPages_.IsManaged()];
+ }
+ else if (*prefName == prefs::kSearchSuggestEnabled) {
+ [self setUseSuggest:useSuggest_.GetValue() ? YES : NO];
+ [self setUseSuggestEnabled:!useSuggest_.IsManaged()];
+ }
+ else if (*prefName == prefs::kDnsPrefetchingEnabled) {
+ [self setDnsPrefetch:dnsPrefetch_.GetValue() ? YES : NO];
+ [self setDnsPrefetchEnabled:!dnsPrefetch_.IsManaged()];
+ }
+ else if (*prefName == prefs::kSafeBrowsingEnabled) {
+ [self setSafeBrowsing:safeBrowsing_.GetValue() ? YES : NO];
+ [self setSafeBrowsingEnabled:!safeBrowsing_.IsManaged()];
+ }
+ else if (*prefName == prefs::kMetricsReportingEnabled) {
+ [self setMetricsReporting:metricsReporting_.GetValue() ? YES : NO];
+ [self setMetricsReportingEnabled:!metricsReporting_.IsManaged()];
+ }
+ else if (*prefName == prefs::kDownloadDefaultDirectory) {
+ // Poke KVO.
+ [self willChangeValueForKey:@"defaultDownloadLocation"];
+ [self didChangeValueForKey:@"defaultDownloadLocation"];
+ }
+ else if (*prefName == prefs::kPromptForDownload) {
+ [self setAskForSaveLocation:askForSaveLocation_.GetValue() ? YES : NO];
+ }
+ else if (*prefName == prefs::kEnableTranslate) {
+ [self setTranslateEnabled:translateEnabled_.GetValue() ? YES : NO];
+ }
+ else if (*prefName == prefs::kWebkitTabsToLinks) {
+ [self setTabsToLinks:tabsToLinks_.GetValue() ? YES : NO];
+ }
+ else if (*prefName == prefs::kDownloadExtensionsToOpen) {
+ // Poke KVC.
+ [self setFileHandlerUIEnabled:[self fileHandlerUIEnabled]];
+ }
+ else if (proxyPrefs_->IsObserved(*prefName)) {
+ [self setProxiesConfigureButtonEnabled:!proxyPrefs_->IsManaged()];
+ }
+}
+
+// Set the new download path and notify the UI via KVO.
+- (void)downloadPathPanelDidEnd:(NSOpenPanel*)panel
+ code:(NSInteger)returnCode
+ context:(void*)context {
+ if (returnCode == NSOKButton) {
+ [self recordUserAction:UserMetricsAction("Options_SetDownloadDirectory")];
+ NSURL* path = [[panel URLs] lastObject]; // We only allow 1 item.
+ [self willChangeValueForKey:@"defaultDownloadLocation"];
+ defaultDownloadLocation_.SetValue(base::SysNSStringToUTF8([path path]));
+ [self didChangeValueForKey:@"defaultDownloadLocation"];
+ }
+}
+
+// Bring up an open panel to allow the user to set a new downloads location.
+- (void)browseDownloadLocation:(id)sender {
+ NSOpenPanel* panel = [NSOpenPanel openPanel];
+ [panel setAllowsMultipleSelection:NO];
+ [panel setCanChooseFiles:NO];
+ [panel setCanChooseDirectories:YES];
+ NSString* path = base::SysUTF8ToNSString(defaultDownloadLocation_.GetValue());
+ [panel beginSheetForDirectory:path
+ file:nil
+ types:nil
+ modalForWindow:[self window]
+ modalDelegate:self
+ didEndSelector:@selector(downloadPathPanelDidEnd:code:context:)
+ contextInfo:NULL];
+}
+
+// Called to clear user's browsing data. This puts up an application-modal
+// dialog to guide the user through clearing the data.
+- (IBAction)clearData:(id)sender {
+ [ClearBrowsingDataController
+ showClearBrowsingDialogForProfile:profile_];
+}
+
+// Opens the "Content Settings" dialog.
+- (IBAction)showContentSettings:(id)sender {
+ [ContentSettingsDialogController
+ showContentSettingsForType:CONTENT_SETTINGS_TYPE_DEFAULT
+ profile:profile_];
+}
+
+- (IBAction)privacyLearnMore:(id)sender {
+ GURL url = google_util::AppendGoogleLocaleParam(
+ GURL(chrome::kPrivacyLearnMoreURL));
+ // We open a new browser window so the Options dialog doesn't get lost
+ // behind other windows.
+ browser::ShowOptionsURL(profile_, url);
+}
+
+- (IBAction)resetAutoOpenFiles:(id)sender {
+ profile_->GetDownloadManager()->download_prefs()->ResetAutoOpen();
+ [self recordUserAction:UserMetricsAction("Options_ResetAutoOpenFiles")];
+}
+
+- (IBAction)openProxyPreferences:(id)sender {
+ NSArray* itemsToOpen = [NSArray arrayWithObject:[NSURL fileURLWithPath:
+ @"/System/Library/PreferencePanes/Network.prefPane"]];
+
+ const char* proxyPrefCommand = "Proxies";
+ base::mac::ScopedAEDesc<> openParams;
+ OSStatus status = AECreateDesc('ptru',
+ proxyPrefCommand,
+ strlen(proxyPrefCommand),
+ openParams.OutPointer());
+ LOG_IF(ERROR, status != noErr) << "Failed to create open params: " << status;
+
+ LSLaunchURLSpec launchSpec = { 0 };
+ launchSpec.itemURLs = (CFArrayRef)itemsToOpen;
+ launchSpec.passThruParams = openParams;
+ launchSpec.launchFlags = kLSLaunchAsync | kLSLaunchDontAddToRecents;
+ LSOpenFromURLSpec(&launchSpec, NULL);
+}
+
+// Returns whether the alternate error page checkbox should be checked based
+// on the preference.
+- (BOOL)showAlternateErrorPages {
+ return alternateErrorPages_.GetValue() ? YES : NO;
+}
+
+// Sets the backend pref for whether or not the alternate error page checkbox
+// should be displayed based on |value|.
+- (void)setShowAlternateErrorPages:(BOOL)value {
+ if (value)
+ [self recordUserAction:UserMetricsAction(
+ "Options_LinkDoctorCheckbox_Enable")];
+ else
+ [self recordUserAction:UserMetricsAction(
+ "Options_LinkDoctorCheckbox_Disable")];
+ alternateErrorPages_.SetValueIfNotManaged(value ? true : false);
+}
+
+// Returns whether the suggest checkbox should be checked based on the
+// preference.
+- (BOOL)useSuggest {
+ return useSuggest_.GetValue() ? YES : NO;
+}
+
+// Sets the backend pref for whether or not the suggest checkbox should be
+// displayed based on |value|.
+- (void)setUseSuggest:(BOOL)value {
+ if (value)
+ [self recordUserAction:UserMetricsAction(
+ "Options_UseSuggestCheckbox_Enable")];
+ else
+ [self recordUserAction:UserMetricsAction(
+ "Options_UseSuggestCheckbox_Disable")];
+ useSuggest_.SetValueIfNotManaged(value ? true : false);
+}
+
+// Returns whether the DNS prefetch checkbox should be checked based on the
+// preference.
+- (BOOL)dnsPrefetch {
+ return dnsPrefetch_.GetValue() ? YES : NO;
+}
+
+// Sets the backend pref for whether or not the DNS prefetch checkbox should be
+// displayed based on |value|.
+- (void)setDnsPrefetch:(BOOL)value {
+ if (value)
+ [self recordUserAction:UserMetricsAction(
+ "Options_DnsPrefetchCheckbox_Enable")];
+ else
+ [self recordUserAction:UserMetricsAction(
+ "Options_DnsPrefetchCheckbox_Disable")];
+ dnsPrefetch_.SetValueIfNotManaged(value ? true : false);
+}
+
+// Returns whether the safe browsing checkbox should be checked based on the
+// preference.
+- (BOOL)safeBrowsing {
+ return safeBrowsing_.GetValue() ? YES : NO;
+}
+
+// Sets the backend pref for whether or not the safe browsing checkbox should be
+// displayed based on |value|.
+- (void)setSafeBrowsing:(BOOL)value {
+ if (value)
+ [self recordUserAction:UserMetricsAction(
+ "Options_SafeBrowsingCheckbox_Enable")];
+ else
+ [self recordUserAction:UserMetricsAction(
+ "Options_SafeBrowsingCheckbox_Disable")];
+ safeBrowsing_.SetValueIfNotManaged(value ? true : false);
+ SafeBrowsingService* safeBrowsingService =
+ g_browser_process->resource_dispatcher_host()->safe_browsing_service();
+ MessageLoop::current()->PostTask(
+ FROM_HERE,
+ NewRunnableMethod(safeBrowsingService,
+ &SafeBrowsingService::OnEnable,
+ safeBrowsing_.GetValue()));
+}
+
+// Returns whether the metrics reporting checkbox should be checked based on the
+// preference.
+- (BOOL)metricsReporting {
+ return metricsReporting_.GetValue() ? YES : NO;
+}
+
+// Sets the backend pref for whether or not the metrics reporting checkbox
+// should be displayed based on |value|.
+- (void)setMetricsReporting:(BOOL)value {
+ if (value)
+ [self recordUserAction:UserMetricsAction(
+ "Options_MetricsReportingCheckbox_Enable")];
+ else
+ [self recordUserAction:UserMetricsAction(
+ "Options_MetricsReportingCheckbox_Disable")];
+
+ // TODO(pinkerton): windows shows a dialog here telling the user they need to
+ // restart for this to take effect. http://crbug.com/34653
+ metricsReporting_.SetValueIfNotManaged(value ? true : false);
+
+ bool enabled = metricsReporting_.GetValue();
+ GoogleUpdateSettings::SetCollectStatsConsent(enabled);
+ bool update_pref = GoogleUpdateSettings::GetCollectStatsConsent();
+ if (enabled != update_pref) {
+ DVLOG(1) << "GENERAL SECTION: Unable to set crash report status to "
+ << enabled;
+ }
+ // Only change the pref if GoogleUpdateSettings::GetCollectStatsConsent
+ // succeeds.
+ enabled = update_pref;
+
+ MetricsService* metrics = g_browser_process->metrics_service();
+ DCHECK(metrics);
+ if (metrics) {
+ metrics->SetUserPermitsUpload(enabled);
+ if (enabled)
+ metrics->Start();
+ else
+ metrics->Stop();
+ }
+}
+
+- (NSURL*)defaultDownloadLocation {
+ NSString* pathString =
+ base::SysUTF8ToNSString(defaultDownloadLocation_.GetValue());
+ return [NSURL fileURLWithPath:pathString];
+}
+
+- (BOOL)askForSaveLocation {
+ return askForSaveLocation_.GetValue();
+}
+
+- (void)setAskForSaveLocation:(BOOL)value {
+ if (value) {
+ [self recordUserAction:UserMetricsAction(
+ "Options_AskForSaveLocation_Enable")];
+ } else {
+ [self recordUserAction:UserMetricsAction(
+ "Options_AskForSaveLocation_Disable")];
+ }
+ askForSaveLocation_.SetValue(value);
+}
+
+- (BOOL)fileHandlerUIEnabled {
+ if (!profile_->GetDownloadManager()) // Not set in unit tests.
+ return NO;
+ return profile_->GetDownloadManager()->download_prefs()->IsAutoOpenUsed();
+}
+
+- (void)setFileHandlerUIEnabled:(BOOL)value {
+ [resetFileHandlersButton_ setEnabled:value];
+}
+
+- (BOOL)translateEnabled {
+ return translateEnabled_.GetValue();
+}
+
+- (void)setTranslateEnabled:(BOOL)value {
+ if (value) {
+ [self recordUserAction:UserMetricsAction("Options_Translate_Enable")];
+ } else {
+ [self recordUserAction:UserMetricsAction("Options_Translate_Disable")];
+ }
+ translateEnabled_.SetValue(value);
+}
+
+- (BOOL)tabsToLinks {
+ return tabsToLinks_.GetValue();
+}
+
+- (void)setTabsToLinks:(BOOL)value {
+ if (value) {
+ [self recordUserAction:UserMetricsAction("Options_TabsToLinks_Enable")];
+ } else {
+ [self recordUserAction:UserMetricsAction("Options_TabsToLinks_Disable")];
+ }
+ tabsToLinks_.SetValue(value);
+}
+
+- (void)fontAndLanguageEndSheet:(NSWindow*)sheet
+ returnCode:(NSInteger)returnCode
+ contextInfo:(void*)context {
+ [sheet close];
+ [sheet orderOut:self];
+ fontLanguageSettings_ = nil;
+}
+
+- (IBAction)changeFontAndLanguageSettings:(id)sender {
+ // Intentionally leak the controller as it will clean itself up when the
+ // sheet closes.
+ fontLanguageSettings_ =
+ [[FontLanguageSettingsController alloc] initWithProfile:profile_];
+ [NSApp beginSheet:[fontLanguageSettings_ window]
+ modalForWindow:[self window]
+ modalDelegate:self
+ didEndSelector:@selector(fontAndLanguageEndSheet:returnCode:contextInfo:)
+ contextInfo:nil];
+}
+
+// Called to launch the Keychain Access app to show the user's stored
+// certificates. Note there's no way to script the app to auto-select the
+// certificates.
+- (IBAction)showCertificates:(id)sender {
+ [self recordUserAction:UserMetricsAction("Options_ManagerCerts")];
+ [self launchKeychainAccess];
+}
+
+- (IBAction)resetToDefaults:(id)sender {
+ // The alert will clean itself up in the did-end selector.
+ NSAlert* alert = [[NSAlert alloc] init];
+ [alert setMessageText:l10n_util::GetNSString(IDS_OPTIONS_RESET_MESSAGE)];
+ NSButton* resetButton = [alert addButtonWithTitle:
+ l10n_util::GetNSString(IDS_OPTIONS_RESET_OKLABEL)];
+ [resetButton setKeyEquivalent:@""];
+ NSButton* cancelButton = [alert addButtonWithTitle:
+ l10n_util::GetNSString(IDS_OPTIONS_RESET_CANCELLABEL)];
+ [cancelButton setKeyEquivalent:@"\r"];
+
+ [alert beginSheetModalForWindow:[self window]
+ modalDelegate:self
+ didEndSelector:@selector(resetToDefaults:returned:context:)
+ contextInfo:nil];
+}
+
+- (void)resetToDefaults:(NSAlert*)alert
+ returned:(NSInteger)code
+ context:(void*)context {
+ if (code == NSAlertFirstButtonReturn) {
+ OptionsUtil::ResetToDefaults(profile_);
+ }
+ [alert autorelease];
+}
+
+//-------------------------------------------------------------------------
+
+// Callback when preferences are changed. |prefName| is the name of the
+// pref that has changed and should not be NULL.
+- (void)prefChanged:(std::string*)prefName {
+ DCHECK(prefName);
+ if (!prefName) return;
+ [self basicsPrefChanged:prefName];
+ [self userDataPrefChanged:prefName];
+ [self underHoodPrefChanged:prefName];
+}
+
+// Callback when sync service state has changed.
+//
+// TODO(akalin): Decomp this out since a lot of it is copied from the
+// Windows version.
+// TODO(akalin): Change the background of the status label/link on error.
+- (void)syncStateChanged {
+ DCHECK(syncService_);
+
+ string16 statusLabel, linkLabel;
+ sync_ui_util::MessageType status =
+ sync_ui_util::GetStatusLabels(syncService_, &statusLabel, &linkLabel);
+ bool managed = syncService_->IsManaged();
+
+ [syncButton_ setEnabled:!syncService_->WizardIsVisible()];
+ NSString* buttonLabel;
+ if (syncService_->HasSyncSetupCompleted()) {
+ buttonLabel = l10n_util::GetNSStringWithFixup(
+ IDS_SYNC_STOP_SYNCING_BUTTON_LABEL);
+ [syncCustomizeButton_ setHidden:false];
+ } else if (syncService_->SetupInProgress()) {
+ buttonLabel = l10n_util::GetNSStringWithFixup(
+ IDS_SYNC_NTP_SETUP_IN_PROGRESS);
+ [syncCustomizeButton_ setHidden:true];
+ } else {
+ buttonLabel = l10n_util::GetNSStringWithFixup(
+ IDS_SYNC_START_SYNC_BUTTON_LABEL);
+ [syncCustomizeButton_ setHidden:true];
+ }
+ [syncCustomizeButton_ setEnabled:!managed];
+ [syncButton_ setTitle:buttonLabel];
+ [syncButton_ setEnabled:!managed];
+
+ [syncStatus_ setStringValue:base::SysUTF16ToNSString(statusLabel)];
+ [syncLink_ setHidden:linkLabel.empty()];
+ [syncLink_ setTitle:base::SysUTF16ToNSString(linkLabel)];
+ [syncLink_ setEnabled:!managed];
+
+ NSButtonCell* syncLinkCell = static_cast<NSButtonCell*>([syncLink_ cell]);
+ if (!syncStatusNoErrorBackgroundColor_) {
+ DCHECK(!syncLinkNoErrorBackgroundColor_);
+ // We assume that the sync controls start off in a non-error
+ // state.
+ syncStatusNoErrorBackgroundColor_.reset(
+ [[syncStatus_ backgroundColor] retain]);
+ syncLinkNoErrorBackgroundColor_.reset(
+ [[syncLinkCell backgroundColor] retain]);
+ }
+ if (status == sync_ui_util::SYNC_ERROR) {
+ [syncStatus_ setBackgroundColor:syncErrorBackgroundColor_];
+ [syncLinkCell setBackgroundColor:syncErrorBackgroundColor_];
+ } else {
+ [syncStatus_ setBackgroundColor:syncStatusNoErrorBackgroundColor_];
+ [syncLinkCell setBackgroundColor:syncLinkNoErrorBackgroundColor_];
+ }
+}
+
+// Show the preferences window.
+- (IBAction)showPreferences:(id)sender {
+ [self showWindow:sender];
+}
+
+- (IBAction)toolbarButtonSelected:(id)sender {
+ DCHECK([sender isKindOfClass:[NSToolbarItem class]]);
+ OptionsPage page = [self getPageForToolbarItem:sender];
+ [self displayPreferenceViewForPage:page animate:YES];
+}
+
+// Helper to update the window to display a preferences view for a page.
+- (void)displayPreferenceViewForPage:(OptionsPage)page
+ animate:(BOOL)animate {
+ NSWindow* prefsWindow = [self window];
+
+ // Needs to go *after* the call to [self window], which triggers
+ // awakeFromNib if necessary.
+ NSView* prefsView = [self getPrefsViewForPage:page];
+ NSView* contentView = [prefsWindow contentView];
+
+ // Make sure we aren't being told to display the same thing again.
+ if (currentPrefsView_ == prefsView &&
+ managedPrefsBannerVisible_ == bannerState_->IsVisible()) {
+ return;
+ }
+
+ // Remember new options page as current page.
+ if (page != OPTIONS_PAGE_DEFAULT)
+ lastSelectedPage_.SetValue(page);
+
+ // Stop any running animation, and reset the subviews to the new state. We
+ // re-add any views we need for animation later.
+ [animation_ stopAnimation];
+ NSView* oldPrefsView = currentPrefsView_;
+ currentPrefsView_ = prefsView;
+ [self resetSubViews];
+
+ // Update the banner state.
+ [self initBannerStateForPage:page];
+ BOOL showBanner = bannerState_->IsVisible();
+
+ // Update the window title.
+ NSToolbarItem* toolbarItem = [self getToolbarItemForPage:page];
+ [prefsWindow setTitle:[toolbarItem label]];
+
+ // Calculate new frames for the subviews.
+ NSRect prefsViewFrame = [prefsView frame];
+ NSRect contentViewFrame = [contentView frame];
+ NSRect bannerViewFrame = [managedPrefsBannerView_ frame];
+
+ // Determine what height the managed prefs banner will use.
+ CGFloat bannerViewHeight = showBanner ? NSHeight(bannerViewFrame) : 0.0;
+
+ if (animate) {
+ // NSViewAnimation doesn't seem to honor subview resizing as it animates the
+ // Window's frame. So instead of trying to get the top in the right place,
+ // just set the origin where it should be at the end, and let the fade/size
+ // slide things into the right spot.
+ prefsViewFrame.origin.y = 0.0;
+ } else {
+ // The prefView is anchored to the top of its parent, so set its origin so
+ // that the top is where it should be. When the window's frame is set, the
+ // origin will be adjusted to keep it in the right spot.
+ prefsViewFrame.origin.y = NSHeight(contentViewFrame) -
+ NSHeight(prefsViewFrame) - bannerViewHeight;
+ }
+ bannerViewFrame.origin.y = NSHeight(prefsViewFrame);
+ bannerViewFrame.size.width = NSWidth(contentViewFrame);
+ [prefsView setFrame:prefsViewFrame];
+
+ // Figure out the size of the window.
+ NSRect windowFrame = [contentView convertRect:[prefsWindow frame]
+ fromView:nil];
+ CGFloat titleToolbarHeight =
+ NSHeight(windowFrame) - NSHeight(contentViewFrame);
+ windowFrame.size.height =
+ NSHeight(prefsViewFrame) + titleToolbarHeight + bannerViewHeight;
+ DCHECK_GE(NSWidth(windowFrame), NSWidth(prefsViewFrame))
+ << "Initial width set wasn't wide enough.";
+ windowFrame = [contentView convertRect:windowFrame toView:nil];
+ windowFrame.origin.y = NSMaxY([prefsWindow frame]) - NSHeight(windowFrame);
+
+ // Now change the size.
+ if (animate) {
+ NSMutableArray* animations = [NSMutableArray arrayWithCapacity:4];
+ if (oldPrefsView != prefsView) {
+ // Fade between prefs views if they change.
+ [contentView addSubview:oldPrefsView
+ positioned:NSWindowBelow
+ relativeTo:nil];
+ [animations addObject:
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ oldPrefsView, NSViewAnimationTargetKey,
+ NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey,
+ nil]];
+ [animations addObject:
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ prefsView, NSViewAnimationTargetKey,
+ NSViewAnimationFadeInEffect, NSViewAnimationEffectKey,
+ nil]];
+ } else {
+ // Make sure the prefs pane ends up in the right position in case we
+ // manipulate the banner.
+ [animations addObject:
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ prefsView, NSViewAnimationTargetKey,
+ [NSValue valueWithRect:prefsViewFrame],
+ NSViewAnimationEndFrameKey,
+ nil]];
+ }
+ if (showBanner != managedPrefsBannerVisible_) {
+ // Slide the warning banner in or out of view.
+ [animations addObject:
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ managedPrefsBannerView_, NSViewAnimationTargetKey,
+ [NSValue valueWithRect:bannerViewFrame],
+ NSViewAnimationEndFrameKey,
+ nil]];
+ }
+ // Window resize animation.
+ [animations addObject:
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ prefsWindow, NSViewAnimationTargetKey,
+ [NSValue valueWithRect:windowFrame], NSViewAnimationEndFrameKey,
+ nil]];
+ [animation_ setViewAnimations:animations];
+ // The default duration is 0.5s, which actually feels slow in here, so speed
+ // it up a bit.
+ [animation_ gtm_setDuration:0.2
+ eventMask:NSLeftMouseUpMask];
+ [animation_ startAnimation];
+ } else {
+ // If not animating, odds are we don't want to display either (because it
+ // is initial window setup).
+ [prefsWindow setFrame:windowFrame display:NO];
+ [managedPrefsBannerView_ setFrame:bannerViewFrame];
+ }
+
+ managedPrefsBannerVisible_ = showBanner;
+}
+
+- (void)resetSubViews {
+ // Reset subviews to current prefs view and banner, remove any views that
+ // might have been left over from previous state or animation.
+ NSArray* subviews = [NSArray arrayWithObjects:
+ currentPrefsView_, managedPrefsBannerView_, nil];
+ [[[self window] contentView] setSubviews:subviews];
+ [[self window] setInitialFirstResponder:currentPrefsView_];
+}
+
+- (void)animationDidEnd:(NSAnimation*)animation {
+ DCHECK_EQ(animation_.get(), animation);
+ // Animation finished, reset subviews to current prefs view and the banner.
+ [self resetSubViews];
+}
+
+// Reinitializes the banner state tracker object to watch for managed bits of
+// preferences relevant to the given options |page|.
+- (void)initBannerStateForPage:(OptionsPage)page {
+ page = [self normalizePage:page];
+
+ // During unit tests, there is no local state object, so we fall back to
+ // the prefs object (where we've explicitly registered this pref so we
+ // know it's there).
+ PrefService* local = g_browser_process->local_state();
+ if (!local)
+ local = prefs_;
+ bannerState_.reset(
+ new PreferencesWindowControllerInternal::ManagedPrefsBannerState(
+ self, page, local, prefs_));
+}
+
+- (void)switchToPage:(OptionsPage)page animate:(BOOL)animate {
+ [self displayPreferenceViewForPage:page animate:animate];
+ NSToolbarItem* toolbarItem = [self getToolbarItemForPage:page];
+ [toolbar_ setSelectedItemIdentifier:[toolbarItem itemIdentifier]];
+}
+
+// Called when the window is being closed. Send out a notification that the user
+// is done editing preferences. Make sure there are no pending field editors
+// by clearing the first responder.
+- (void)windowWillClose:(NSNotification*)notification {
+ // Setting the first responder to the window ends any in-progress field
+ // editor. This will update the model appropriately so there's nothing left
+ // to do.
+ if (![[self window] makeFirstResponder:[self window]]) {
+ // We've hit a recalcitrant field editor, force it to go away.
+ [[self window] endEditingFor:nil];
+ }
+ [self autorelease];
+}
+
+- (void)controlTextDidEndEditing:(NSNotification*)notification {
+ [customPagesSource_ validateURLs];
+}
+
+@end
+
+@implementation PreferencesWindowController(Testing)
+
+- (IntegerPrefMember*)lastSelectedPage {
+ return &lastSelectedPage_;
+}
+
+- (NSToolbar*)toolbar {
+ return toolbar_;
+}
+
+- (NSView*)basicsView {
+ return basicsView_;
+}
+
+- (NSView*)personalStuffView {
+ return personalStuffView_;
+}
+
+- (NSView*)underTheHoodView {
+ return underTheHoodView_;
+}
+
+- (OptionsPage)normalizePage:(OptionsPage)page {
+ if (page == OPTIONS_PAGE_DEFAULT) {
+ // Get the last visited page from local state.
+ page = static_cast<OptionsPage>(lastSelectedPage_.GetValue());
+ if (page == OPTIONS_PAGE_DEFAULT) {
+ page = OPTIONS_PAGE_GENERAL;
+ }
+ }
+ return page;
+}
+
+- (NSToolbarItem*)getToolbarItemForPage:(OptionsPage)page {
+ NSUInteger pageIndex = (NSUInteger)[self normalizePage:page];
+ NSArray* items = [toolbar_ items];
+ NSUInteger itemCount = [items count];
+ DCHECK_GE(pageIndex, 0U);
+ if (pageIndex >= itemCount) {
+ NOTIMPLEMENTED();
+ pageIndex = 0;
+ }
+ DCHECK_GT(itemCount, 0U);
+ return [items objectAtIndex:pageIndex];
+}
+
+- (OptionsPage)getPageForToolbarItem:(NSToolbarItem*)toolbarItem {
+ // Tags are set in the nib file.
+ switch ([toolbarItem tag]) {
+ case 0: // Basics
+ return OPTIONS_PAGE_GENERAL;
+ case 1: // Personal Stuff
+ return OPTIONS_PAGE_CONTENT;
+ case 2: // Under the Hood
+ return OPTIONS_PAGE_ADVANCED;
+ default:
+ NOTIMPLEMENTED();
+ return OPTIONS_PAGE_GENERAL;
+ }
+}
+
+- (NSView*)getPrefsViewForPage:(OptionsPage)page {
+ // The views will be NULL if this is mistakenly called before awakeFromNib.
+ DCHECK(basicsView_);
+ DCHECK(personalStuffView_);
+ DCHECK(underTheHoodView_);
+ page = [self normalizePage:page];
+ switch (page) {
+ case OPTIONS_PAGE_GENERAL:
+ return basicsView_;
+ case OPTIONS_PAGE_CONTENT:
+ return personalStuffView_;
+ case OPTIONS_PAGE_ADVANCED:
+ return underTheHoodView_;
+ case OPTIONS_PAGE_DEFAULT:
+ case OPTIONS_PAGE_COUNT:
+ LOG(DFATAL) << "Invalid page value " << page;
+ }
+ return basicsView_;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/preferences_window_controller_unittest.mm b/chrome/browser/ui/cocoa/preferences_window_controller_unittest.mm
new file mode 100644
index 0000000..91993c2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/preferences_window_controller_unittest.mm
@@ -0,0 +1,240 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/scoped_nsobject.h"
+#include "chrome/browser/options_window.h"
+#import "chrome/browser/ui/cocoa/preferences_window_controller.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/custom_home_pages_model.h"
+#include "chrome/common/pref_names.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+// Helper Objective-C object that sets a BOOL when we get a particular
+// callback from the prefs window.
+@interface PrefsClosedObserver : NSObject {
+ @public
+ BOOL gotNotification_;
+}
+- (void)prefsWindowClosed:(NSNotification*)notify;
+@end
+
+@implementation PrefsClosedObserver
+- (void)prefsWindowClosed:(NSNotification*)notify {
+ gotNotification_ = YES;
+}
+@end
+
+namespace {
+
+class PrefsControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ // The metrics reporting pref is registerd on the local state object in
+ // real builds, but we don't have one of those for unit tests. Register
+ // it on prefs so we'll find it when we go looking.
+ PrefService* prefs = browser_helper_.profile()->GetPrefs();
+ prefs->RegisterBooleanPref(prefs::kMetricsReportingEnabled, false);
+
+ pref_controller_ = [[PreferencesWindowController alloc]
+ initWithProfile:browser_helper_.profile()
+ initialPage:OPTIONS_PAGE_DEFAULT];
+ EXPECT_TRUE(pref_controller_);
+ }
+
+ virtual void TearDown() {
+ [pref_controller_ close];
+ CocoaTest::TearDown();
+ }
+
+ BrowserTestHelper browser_helper_;
+ PreferencesWindowController* pref_controller_;
+};
+
+// Test showing the preferences window and making sure it's visible, then
+// making sure we get the notification when it's closed.
+TEST_F(PrefsControllerTest, ShowAndClose) {
+ [pref_controller_ showPreferences:nil];
+ EXPECT_TRUE([[pref_controller_ window] isVisible]);
+
+ scoped_nsobject<PrefsClosedObserver> observer(
+ [[PrefsClosedObserver alloc] init]);
+ NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
+ [defaultCenter addObserver:observer.get()
+ selector:@selector(prefsWindowClosed:)
+ name:NSWindowWillCloseNotification
+ object:[pref_controller_ window]];
+ [[pref_controller_ window] performClose:observer];
+ EXPECT_TRUE(observer.get()->gotNotification_);
+ [defaultCenter removeObserver:observer.get()];
+
+ // Prevent pref_controller_ from being closed again in TearDown()
+ pref_controller_ = nil;
+}
+
+TEST_F(PrefsControllerTest, ValidateCustomHomePagesTable) {
+ // First, insert two valid URLs into the CustomHomePagesModel.
+ GURL url1("http://www.google.com/");
+ GURL url2("http://maps.google.com/");
+ std::vector<GURL> urls;
+ urls.push_back(url1);
+ urls.push_back(url2);
+ [[pref_controller_ customPagesSource] setURLs:urls];
+ EXPECT_EQ(2U, [[pref_controller_ customPagesSource] countOfCustomHomePages]);
+
+ // Now insert a bad (empty) URL into the model.
+ [[pref_controller_ customPagesSource] setURLStringEmptyAt:1];
+
+ // Send a notification to simulate the end of editing on a cell in the table
+ // which should trigger validation.
+ [pref_controller_ controlTextDidEndEditing:[NSNotification
+ notificationWithName:NSControlTextDidEndEditingNotification
+ object:nil]];
+ EXPECT_EQ(1U, [[pref_controller_ customPagesSource] countOfCustomHomePages]);
+}
+
+TEST_F(PrefsControllerTest, NormalizePage) {
+ EXPECT_EQ(OPTIONS_PAGE_GENERAL,
+ [pref_controller_ normalizePage:OPTIONS_PAGE_GENERAL]);
+ EXPECT_EQ(OPTIONS_PAGE_CONTENT,
+ [pref_controller_ normalizePage:OPTIONS_PAGE_CONTENT]);
+ EXPECT_EQ(OPTIONS_PAGE_ADVANCED,
+ [pref_controller_ normalizePage:OPTIONS_PAGE_ADVANCED]);
+
+ [pref_controller_ lastSelectedPage]->SetValue(OPTIONS_PAGE_ADVANCED);
+ EXPECT_EQ(OPTIONS_PAGE_ADVANCED,
+ [pref_controller_ normalizePage:OPTIONS_PAGE_DEFAULT]);
+
+ [pref_controller_ lastSelectedPage]->SetValue(OPTIONS_PAGE_DEFAULT);
+ EXPECT_EQ(OPTIONS_PAGE_GENERAL,
+ [pref_controller_ normalizePage:OPTIONS_PAGE_DEFAULT]);
+}
+
+TEST_F(PrefsControllerTest, GetToolbarItemForPage) {
+ // Trigger awakeFromNib.
+ [pref_controller_ window];
+
+ NSArray* toolbarItems = [[pref_controller_ toolbar] items];
+ EXPECT_EQ([toolbarItems objectAtIndex:0],
+ [pref_controller_ getToolbarItemForPage:OPTIONS_PAGE_GENERAL]);
+ EXPECT_EQ([toolbarItems objectAtIndex:1],
+ [pref_controller_ getToolbarItemForPage:OPTIONS_PAGE_CONTENT]);
+ EXPECT_EQ([toolbarItems objectAtIndex:2],
+ [pref_controller_ getToolbarItemForPage:OPTIONS_PAGE_ADVANCED]);
+
+ [pref_controller_ lastSelectedPage]->SetValue(OPTIONS_PAGE_ADVANCED);
+ EXPECT_EQ([toolbarItems objectAtIndex:2],
+ [pref_controller_ getToolbarItemForPage:OPTIONS_PAGE_DEFAULT]);
+
+ // Out-of-range argument.
+ EXPECT_EQ([toolbarItems objectAtIndex:0],
+ [pref_controller_ getToolbarItemForPage:OPTIONS_PAGE_COUNT]);
+}
+
+TEST_F(PrefsControllerTest, GetPageForToolbarItem) {
+ scoped_nsobject<NSToolbarItem> toolbarItem(
+ [[NSToolbarItem alloc] initWithItemIdentifier:@""]);
+ [toolbarItem setTag:0];
+ EXPECT_EQ(OPTIONS_PAGE_GENERAL,
+ [pref_controller_ getPageForToolbarItem:toolbarItem]);
+ [toolbarItem setTag:1];
+ EXPECT_EQ(OPTIONS_PAGE_CONTENT,
+ [pref_controller_ getPageForToolbarItem:toolbarItem]);
+ [toolbarItem setTag:2];
+ EXPECT_EQ(OPTIONS_PAGE_ADVANCED,
+ [pref_controller_ getPageForToolbarItem:toolbarItem]);
+
+ // Out-of-range argument.
+ [toolbarItem setTag:3];
+ EXPECT_EQ(OPTIONS_PAGE_GENERAL,
+ [pref_controller_ getPageForToolbarItem:toolbarItem]);
+}
+
+TEST_F(PrefsControllerTest, GetPrefsViewForPage) {
+ // Trigger awakeFromNib.
+ [pref_controller_ window];
+
+ EXPECT_EQ([pref_controller_ basicsView],
+ [pref_controller_ getPrefsViewForPage:OPTIONS_PAGE_GENERAL]);
+ EXPECT_EQ([pref_controller_ personalStuffView],
+ [pref_controller_ getPrefsViewForPage:OPTIONS_PAGE_CONTENT]);
+ EXPECT_EQ([pref_controller_ underTheHoodView],
+ [pref_controller_ getPrefsViewForPage:OPTIONS_PAGE_ADVANCED]);
+
+ [pref_controller_ lastSelectedPage]->SetValue(OPTIONS_PAGE_ADVANCED);
+ EXPECT_EQ([pref_controller_ underTheHoodView],
+ [pref_controller_ getPrefsViewForPage:OPTIONS_PAGE_DEFAULT]);
+}
+
+TEST_F(PrefsControllerTest, SwitchToPage) {
+ // Trigger awakeFromNib.
+ NSWindow* window = [pref_controller_ window];
+
+ NSView* contentView = [window contentView];
+ NSView* basicsView = [pref_controller_ basicsView];
+ NSView* personalStuffView = [pref_controller_ personalStuffView];
+ NSView* underTheHoodView = [pref_controller_ underTheHoodView];
+ NSToolbar* toolbar = [pref_controller_ toolbar];
+ NSToolbarItem* basicsToolbarItem =
+ [pref_controller_ getToolbarItemForPage:OPTIONS_PAGE_GENERAL];
+ NSToolbarItem* personalStuffToolbarItem =
+ [pref_controller_ getToolbarItemForPage:OPTIONS_PAGE_CONTENT];
+ NSToolbarItem* underTheHoodToolbarItem =
+ [pref_controller_ getToolbarItemForPage:OPTIONS_PAGE_ADVANCED];
+ NSString* basicsIdentifier = [basicsToolbarItem itemIdentifier];
+ NSString* personalStuffIdentifier = [personalStuffToolbarItem itemIdentifier];
+ NSString* underTheHoodIdentifier = [underTheHoodToolbarItem itemIdentifier];
+ IntegerPrefMember* lastSelectedPage = [pref_controller_ lastSelectedPage];
+
+ // Test without animation.
+
+ [pref_controller_ switchToPage:OPTIONS_PAGE_GENERAL animate:NO];
+ EXPECT_TRUE([basicsView isDescendantOf:contentView]);
+ EXPECT_FALSE([personalStuffView isDescendantOf:contentView]);
+ EXPECT_FALSE([underTheHoodView isDescendantOf:contentView]);
+ EXPECT_NSEQ(basicsIdentifier, [toolbar selectedItemIdentifier]);
+ EXPECT_EQ(OPTIONS_PAGE_GENERAL, lastSelectedPage->GetValue());
+ EXPECT_NSEQ([basicsToolbarItem label], [window title]);
+
+ [pref_controller_ switchToPage:OPTIONS_PAGE_CONTENT animate:NO];
+ EXPECT_FALSE([basicsView isDescendantOf:contentView]);
+ EXPECT_TRUE([personalStuffView isDescendantOf:contentView]);
+ EXPECT_FALSE([underTheHoodView isDescendantOf:contentView]);
+ EXPECT_NSEQ([toolbar selectedItemIdentifier], personalStuffIdentifier);
+ EXPECT_EQ(OPTIONS_PAGE_CONTENT, lastSelectedPage->GetValue());
+ EXPECT_NSEQ([personalStuffToolbarItem label], [window title]);
+
+ [pref_controller_ switchToPage:OPTIONS_PAGE_ADVANCED animate:NO];
+ EXPECT_FALSE([basicsView isDescendantOf:contentView]);
+ EXPECT_FALSE([personalStuffView isDescendantOf:contentView]);
+ EXPECT_TRUE([underTheHoodView isDescendantOf:contentView]);
+ EXPECT_NSEQ([toolbar selectedItemIdentifier], underTheHoodIdentifier);
+ EXPECT_EQ(OPTIONS_PAGE_ADVANCED, lastSelectedPage->GetValue());
+ EXPECT_NSEQ([underTheHoodToolbarItem label], [window title]);
+
+ // Test OPTIONS_PAGE_DEFAULT.
+
+ lastSelectedPage->SetValue(OPTIONS_PAGE_CONTENT);
+ [pref_controller_ switchToPage:OPTIONS_PAGE_DEFAULT animate:NO];
+ EXPECT_FALSE([basicsView isDescendantOf:contentView]);
+ EXPECT_TRUE([personalStuffView isDescendantOf:contentView]);
+ EXPECT_FALSE([underTheHoodView isDescendantOf:contentView]);
+ EXPECT_NSEQ(personalStuffIdentifier, [toolbar selectedItemIdentifier]);
+ EXPECT_EQ(OPTIONS_PAGE_CONTENT, lastSelectedPage->GetValue());
+ EXPECT_NSEQ([personalStuffToolbarItem label], [window title]);
+
+ // TODO(akalin): Figure out how to test animation; we'll need everything
+ // to stick around until the animation finishes.
+}
+
+// TODO(akalin): Figure out how to test sync controls.
+// TODO(akalin): Figure out how to test that sync controls are not shown
+// when there isn't a sync service.
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/previewable_contents_controller.h b/chrome/browser/ui/cocoa/previewable_contents_controller.h
new file mode 100644
index 0000000..01643f0
--- /dev/null
+++ b/chrome/browser/ui/cocoa/previewable_contents_controller.h
@@ -0,0 +1,47 @@
+// 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_UI_COCOA_PREVIEWABLE_CONTENTS_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_PREVIEWABLE_CONTENTS_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+class TabContents;
+
+// PreviewableContentsController manages the display of up to two tab contents
+// views. It is primarily for use with Instant results. This class supports
+// the notion of an "active" view vs. a "preview" tab contents view.
+//
+// The "active" view is a container view that can be retrieved using
+// |-activeContainer|. Its contents are meant to be managed by an external
+// class.
+//
+// The "preview" can be set using |-showPreview:| and |-hidePreview|. When a
+// preview is set, the active view is hidden (but stays in the view hierarchy).
+// When the preview is removed, the active view is reshown.
+@interface PreviewableContentsController : NSViewController {
+ @private
+ // Container view for the "active" contents.
+ IBOutlet NSView* activeContainer_;
+
+ // The preview TabContents. Will be NULL if no preview is currently showing.
+ TabContents* previewContents_; // weak
+}
+
+@property(readonly, nonatomic) NSView* activeContainer;
+
+// Sets the current preview and installs its TabContentsView into the view
+// hierarchy. Hides the active view. |preview| must not be NULL.
+- (void)showPreview:(TabContents*)preview;
+
+// Closes the current preview and shows the active view.
+- (void)hidePreview;
+
+// Returns YES if the preview contents is currently showing.
+- (BOOL)isShowingPreview;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_PREVIEWABLE_CONTENTS_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/previewable_contents_controller.mm b/chrome/browser/ui/cocoa/previewable_contents_controller.mm
new file mode 100644
index 0000000..2dfa146
--- /dev/null
+++ b/chrome/browser/ui/cocoa/previewable_contents_controller.mm
@@ -0,0 +1,52 @@
+// 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/ui/cocoa/previewable_contents_controller.h"
+
+#include "base/logging.h"
+#include "base/mac_util.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+
+@implementation PreviewableContentsController
+
+@synthesize activeContainer = activeContainer_;
+
+- (id)init {
+ if ((self = [super initWithNibName:@"PreviewableContents"
+ bundle:mac_util::MainAppBundle()])) {
+ }
+ return self;
+}
+
+- (void)showPreview:(TabContents*)preview {
+ DCHECK(preview);
+
+ // Remove any old preview contents before showing the new one.
+ if (previewContents_)
+ [previewContents_->GetNativeView() removeFromSuperview];
+
+ previewContents_ = preview;
+ NSView* previewView = previewContents_->GetNativeView();
+ [previewView setFrame:[[self view] bounds]];
+
+ // Hide the active container and add the preview contents.
+ [activeContainer_ setHidden:YES];
+ [[self view] addSubview:previewView];
+}
+
+- (void)hidePreview {
+ DCHECK(previewContents_);
+
+ // Remove the preview contents and reshow the active container.
+ [previewContents_->GetNativeView() removeFromSuperview];
+ [activeContainer_ setHidden:NO];
+
+ previewContents_ = nil;
+}
+
+- (BOOL)isShowingPreview {
+ return previewContents_ != nil;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/previewable_contents_controller_unittest.mm b/chrome/browser/ui/cocoa/previewable_contents_controller_unittest.mm
new file mode 100644
index 0000000..a2d9263
--- /dev/null
+++ b/chrome/browser/ui/cocoa/previewable_contents_controller_unittest.mm
@@ -0,0 +1,34 @@
+// 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 "base/scoped_nsobject.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/previewable_contents_controller.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class PreviewableContentsControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ controller_.reset([[PreviewableContentsController alloc] init]);
+ [[test_window() contentView] addSubview:[controller_ view]];
+ }
+
+ scoped_nsobject<PreviewableContentsController> controller_;
+};
+
+TEST_VIEW(PreviewableContentsControllerTest, [controller_ view])
+
+// TODO(rohitrao): Test showing and hiding the preview. This may require
+// changing the interface to take in a TabContentsView* instead of a
+// TabContents*.
+
+} // namespace
+
diff --git a/chrome/browser/ui/cocoa/reload_button.h b/chrome/browser/ui/cocoa/reload_button.h
new file mode 100644
index 0000000..f955590
--- /dev/null
+++ b/chrome/browser/ui/cocoa/reload_button.h
@@ -0,0 +1,50 @@
+// 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_UI_COCOA_RELOAD_BUTTON_H_
+#define CHROME_BROWSER_UI_COCOA_RELOAD_BUTTON_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/scoped_nsobject.h"
+
+// NSButton subclass which defers certain state changes when the mouse
+// is hovering over it.
+
+@interface ReloadButton : NSButton {
+ @private
+ // Tracks whether the mouse is hovering for purposes of not making
+ // unexpected state changes.
+ BOOL isMouseInside_;
+ scoped_nsobject<NSTrackingArea> trackingArea_;
+
+ // Timer used when setting reload mode while the mouse is hovered.
+ scoped_nsobject<NSTimer> pendingReloadTimer_;
+}
+
+// Returns YES if the mouse is currently inside the bounds.
+- (BOOL)isMouseInside;
+
+// Update the tag, and the image and tooltip to match. If |anInt|
+// matches the current tag, no action is taken. |anInt| must be
+// either |IDC_STOP| or |IDC_RELOAD|.
+- (void)updateTag:(NSInteger)anInt;
+
+// Update the button to be a reload button or stop button depending on
+// |isLoading|. If |force|, always sets the indicated mode. If
+// |!force|, and the mouse is over the button, defer the transition
+// from stop button to reload button until the mouse has left the
+// button, or until |pendingReloadTimer_| fires. This prevents an
+// inadvertent click _just_ as the state changes.
+- (void)setIsLoading:(BOOL)isLoading force:(BOOL)force;
+
+@end
+
+@interface ReloadButton (PrivateTestingMethods)
++ (void)setPendingReloadTimeout:(NSTimeInterval)seconds;
+- (NSTrackingArea*)trackingArea;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_RELOAD_BUTTON_H_
diff --git a/chrome/browser/ui/cocoa/reload_button.mm b/chrome/browser/ui/cocoa/reload_button.mm
new file mode 100644
index 0000000..84e7091
--- /dev/null
+++ b/chrome/browser/ui/cocoa/reload_button.mm
@@ -0,0 +1,168 @@
+// 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/ui/cocoa/reload_button.h"
+
+#include "app/l10n_util.h"
+#include "app/l10n_util_mac.h"
+#include "base/nsimage_cache_mac.h"
+#include "chrome/app/chrome_command_ids.h"
+#import "chrome/browser/ui/cocoa/gradient_button_cell.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+#include "grit/generated_resources.h"
+
+namespace {
+
+NSString* const kReloadImageName = @"reload_Template.pdf";
+NSString* const kStopImageName = @"stop_Template.pdf";
+
+// Constant matches Windows.
+NSTimeInterval kPendingReloadTimeout = 1.35;
+
+} // namespace
+
+@implementation ReloadButton
+
+- (void)dealloc {
+ if (trackingArea_) {
+ [self removeTrackingArea:trackingArea_];
+ trackingArea_.reset();
+ }
+ [super dealloc];
+}
+
+- (void)updateTrackingAreas {
+ // If the mouse is hovering when the tracking area is updated, the
+ // control could end up locked into inappropriate behavior for
+ // awhile, so unwind state.
+ if (isMouseInside_)
+ [self mouseExited:nil];
+
+ if (trackingArea_) {
+ [self removeTrackingArea:trackingArea_];
+ trackingArea_.reset();
+ }
+ trackingArea_.reset([[NSTrackingArea alloc]
+ initWithRect:[self bounds]
+ options:(NSTrackingMouseEnteredAndExited |
+ NSTrackingActiveInActiveApp)
+ owner:self
+ userInfo:nil]);
+ [self addTrackingArea:trackingArea_];
+}
+
+- (void)awakeFromNib {
+ [self updateTrackingAreas];
+
+ // Don't allow multi-clicks, because the user probably wouldn't ever
+ // want to stop+reload or reload+stop.
+ [self setIgnoresMultiClick:YES];
+}
+
+- (void)updateTag:(NSInteger)anInt {
+ if ([self tag] == anInt)
+ return;
+
+ // Forcibly remove any stale tooltip which is being displayed.
+ [self removeAllToolTips];
+
+ [self setTag:anInt];
+ if (anInt == IDC_RELOAD) {
+ [self setImage:nsimage_cache::ImageNamed(kReloadImageName)];
+ [self setToolTip:l10n_util::GetNSStringWithFixup(IDS_TOOLTIP_RELOAD)];
+ } else if (anInt == IDC_STOP) {
+ [self setImage:nsimage_cache::ImageNamed(kStopImageName)];
+ [self setToolTip:l10n_util::GetNSStringWithFixup(IDS_TOOLTIP_STOP)];
+ } else {
+ NOTREACHED();
+ }
+}
+
+- (void)setIsLoading:(BOOL)isLoading force:(BOOL)force {
+ // Can always transition to stop mode. Only transition to reload
+ // mode if forced or if the mouse isn't hovering. Otherwise, note
+ // that reload mode is desired and disable the button.
+ if (isLoading) {
+ pendingReloadTimer_.reset();
+ [self updateTag:IDC_STOP];
+ [self setEnabled:YES];
+ } else if (force || ![self isMouseInside]) {
+ pendingReloadTimer_.reset();
+ [self updateTag:IDC_RELOAD];
+
+ // This button's cell may not have received a mouseExited event, and
+ // therefore it could still think that the mouse is inside the button. Make
+ // sure the cell's sense of mouse-inside matches the local sense, to prevent
+ // drawing artifacts.
+ id cell = [self cell];
+ if ([cell respondsToSelector:@selector(setMouseInside:animate:)])
+ [cell setMouseInside:[self isMouseInside] animate:NO];
+ [self setEnabled:YES];
+ } else if ([self tag] == IDC_STOP && !pendingReloadTimer_) {
+ [self setEnabled:NO];
+ pendingReloadTimer_.reset(
+ [[NSTimer scheduledTimerWithTimeInterval:kPendingReloadTimeout
+ target:self
+ selector:@selector(forceReloadState)
+ userInfo:nil
+ repeats:NO] retain]);
+ }
+}
+
+- (void)forceReloadState {
+ [self setIsLoading:NO force:YES];
+}
+
+- (BOOL)sendAction:(SEL)theAction to:(id)theTarget {
+ if ([self tag] == IDC_STOP) {
+ // When the timer is started, the button is disabled, so this
+ // should not be possible.
+ DCHECK(!pendingReloadTimer_.get());
+
+ // When the stop is processed, immediately change to reload mode,
+ // even though the IPC still has to bounce off the renderer and
+ // back before the regular |-setIsLoaded:force:| will be called.
+ // [This is how views and gtk do it.]
+ const BOOL ret = [super sendAction:theAction to:theTarget];
+ if (ret)
+ [self forceReloadState];
+ return ret;
+ }
+
+ return [super sendAction:theAction to:theTarget];
+}
+
+- (void)mouseEntered:(NSEvent*)theEvent {
+ isMouseInside_ = YES;
+}
+
+- (void)mouseExited:(NSEvent*)theEvent {
+ isMouseInside_ = NO;
+
+ // Reload mode was requested during the hover.
+ if (pendingReloadTimer_)
+ [self forceReloadState];
+}
+
+- (BOOL)isMouseInside {
+ return isMouseInside_;
+}
+
+- (ViewID)viewID {
+ return VIEW_ID_RELOAD_BUTTON;
+}
+
+@end // ReloadButton
+
+@implementation ReloadButton (Testing)
+
++ (void)setPendingReloadTimeout:(NSTimeInterval)seconds {
+ kPendingReloadTimeout = seconds;
+}
+
+- (NSTrackingArea*)trackingArea {
+ return trackingArea_;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/reload_button_unittest.mm b/chrome/browser/ui/cocoa/reload_button_unittest.mm
new file mode 100644
index 0000000..386a503
--- /dev/null
+++ b/chrome/browser/ui/cocoa/reload_button_unittest.mm
@@ -0,0 +1,259 @@
+// 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/ui/cocoa/reload_button.h"
+
+#include "base/scoped_nsobject.h"
+#include "chrome/app/chrome_command_ids.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/test_event_utils.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+
+@protocol TargetActionMock <NSObject>
+- (void)anAction:(id)sender;
+@end
+
+namespace {
+
+class ReloadButtonTest : public CocoaTest {
+ public:
+ ReloadButtonTest() {
+ NSRect frame = NSMakeRect(0, 0, 20, 20);
+ scoped_nsobject<ReloadButton> button(
+ [[ReloadButton alloc] initWithFrame:frame]);
+ button_ = button.get();
+
+ // Set things up so unit tests have a reliable baseline.
+ [button_ setTag:IDC_RELOAD];
+ [button_ awakeFromNib];
+
+ [[test_window() contentView] addSubview:button_];
+ }
+
+ ReloadButton* button_;
+};
+
+TEST_VIEW(ReloadButtonTest, button_)
+
+// Test that mouse-tracking is setup and does the right thing.
+TEST_F(ReloadButtonTest, IsMouseInside) {
+ EXPECT_TRUE([[button_ trackingAreas] containsObject:[button_ trackingArea]]);
+
+ EXPECT_FALSE([button_ isMouseInside]);
+ [button_ mouseEntered:nil];
+ EXPECT_TRUE([button_ isMouseInside]);
+ [button_ mouseExited:nil];
+}
+
+// Verify that multiple clicks do not result in multiple messages to
+// the target.
+TEST_F(ReloadButtonTest, IgnoredMultiClick) {
+ id mock_target = [OCMockObject mockForProtocol:@protocol(TargetActionMock)];
+ [button_ setTarget:mock_target];
+ [button_ setAction:@selector(anAction:)];
+
+ // Expect the action once.
+ [[mock_target expect] anAction:button_];
+
+ const std::pair<NSEvent*,NSEvent*> click_one =
+ test_event_utils::MouseClickInView(button_, 1);
+ const std::pair<NSEvent*,NSEvent*> click_two =
+ test_event_utils::MouseClickInView(button_, 2);
+ [NSApp postEvent:click_one.second atStart:YES];
+ [button_ mouseDown:click_one.first];
+ [NSApp postEvent:click_two.second atStart:YES];
+ [button_ mouseDown:click_two.first];
+
+ [button_ setTarget:nil];
+}
+
+TEST_F(ReloadButtonTest, UpdateTag) {
+ [button_ setTag:IDC_STOP];
+
+ [button_ updateTag:IDC_RELOAD];
+ EXPECT_EQ(IDC_RELOAD, [button_ tag]);
+ NSImage* reloadImage = [button_ image];
+ NSString* const reloadToolTip = [button_ toolTip];
+
+ [button_ updateTag:IDC_STOP];
+ EXPECT_EQ(IDC_STOP, [button_ tag]);
+ NSImage* stopImage = [button_ image];
+ NSString* const stopToolTip = [button_ toolTip];
+ EXPECT_NSNE(reloadImage, stopImage);
+ EXPECT_NSNE(reloadToolTip, stopToolTip);
+
+ [button_ updateTag:IDC_RELOAD];
+ EXPECT_EQ(IDC_RELOAD, [button_ tag]);
+ EXPECT_NSEQ(reloadImage, [button_ image]);
+ EXPECT_NSEQ(reloadToolTip, [button_ toolTip]);
+}
+
+// Test that when forcing the mode, it takes effect immediately,
+// regardless of whether the mouse is hovering.
+TEST_F(ReloadButtonTest, SetIsLoadingForce) {
+ EXPECT_FALSE([button_ isMouseInside]);
+ EXPECT_EQ(IDC_RELOAD, [button_ tag]);
+
+ // Changes to stop immediately.
+ [button_ setIsLoading:YES force:YES];
+ EXPECT_EQ(IDC_STOP, [button_ tag]);
+
+ // Changes to reload immediately.
+ [button_ setIsLoading:NO force:YES];
+ EXPECT_EQ(IDC_RELOAD, [button_ tag]);
+
+ // Changes to stop immediately when the mouse is hovered, and
+ // doesn't change when the mouse exits.
+ [button_ mouseEntered:nil];
+ EXPECT_TRUE([button_ isMouseInside]);
+ [button_ setIsLoading:YES force:YES];
+ EXPECT_EQ(IDC_STOP, [button_ tag]);
+ [button_ mouseExited:nil];
+ EXPECT_FALSE([button_ isMouseInside]);
+ EXPECT_EQ(IDC_STOP, [button_ tag]);
+
+ // Changes to reload immediately when the mouse is hovered, and
+ // doesn't change when the mouse exits.
+ [button_ mouseEntered:nil];
+ EXPECT_TRUE([button_ isMouseInside]);
+ [button_ setIsLoading:NO force:YES];
+ EXPECT_EQ(IDC_RELOAD, [button_ tag]);
+ [button_ mouseExited:nil];
+ EXPECT_FALSE([button_ isMouseInside]);
+ EXPECT_EQ(IDC_RELOAD, [button_ tag]);
+}
+
+// Test that without force, stop mode is set immediately, but reload
+// is affected by the hover status.
+TEST_F(ReloadButtonTest, SetIsLoadingNoForceUnHover) {
+ EXPECT_FALSE([button_ isMouseInside]);
+ EXPECT_EQ(IDC_RELOAD, [button_ tag]);
+
+ // Changes to stop immediately when the mouse is not hovering.
+ [button_ setIsLoading:YES force:NO];
+ EXPECT_EQ(IDC_STOP, [button_ tag]);
+
+ // Changes to reload immediately when the mouse is not hovering.
+ [button_ setIsLoading:NO force:NO];
+ EXPECT_EQ(IDC_RELOAD, [button_ tag]);
+
+ // Changes to stop immediately when the mouse is hovered, and
+ // doesn't change when the mouse exits.
+ [button_ mouseEntered:nil];
+ EXPECT_TRUE([button_ isMouseInside]);
+ [button_ setIsLoading:YES force:NO];
+ EXPECT_EQ(IDC_STOP, [button_ tag]);
+ [button_ mouseExited:nil];
+ EXPECT_FALSE([button_ isMouseInside]);
+ EXPECT_EQ(IDC_STOP, [button_ tag]);
+
+ // Does not change to reload immediately when the mouse is hovered,
+ // changes when the mouse exits.
+ [button_ mouseEntered:nil];
+ EXPECT_TRUE([button_ isMouseInside]);
+ [button_ setIsLoading:NO force:NO];
+ EXPECT_EQ(IDC_STOP, [button_ tag]);
+ [button_ mouseExited:nil];
+ EXPECT_FALSE([button_ isMouseInside]);
+ EXPECT_EQ(IDC_RELOAD, [button_ tag]);
+}
+
+// Test that without force, stop mode is set immediately, and reload
+// will be set after a timeout.
+// TODO(shess): Reenable, http://crbug.com/61485
+TEST_F(ReloadButtonTest, DISABLED_SetIsLoadingNoForceTimeout) {
+ // When the event loop first spins, some delayed tracking-area setup
+ // is done, which causes -mouseExited: to be called. Spin it at
+ // least once, and dequeue any pending events.
+ // TODO(shess): It would be more reasonable to have an MockNSTimer
+ // factory for the class to use, which this code could fire
+ // directly.
+ while ([NSApp nextEventMatchingMask:NSAnyEventMask
+ untilDate:nil
+ inMode:NSDefaultRunLoopMode
+ dequeue:YES]) {
+ }
+
+ const NSTimeInterval kShortTimeout = 0.1;
+ [ReloadButton setPendingReloadTimeout:kShortTimeout];
+
+ EXPECT_FALSE([button_ isMouseInside]);
+ EXPECT_EQ(IDC_RELOAD, [button_ tag]);
+
+ // Move the mouse into the button and press it.
+ [button_ mouseEntered:nil];
+ EXPECT_TRUE([button_ isMouseInside]);
+ [button_ setIsLoading:YES force:NO];
+ EXPECT_EQ(IDC_STOP, [button_ tag]);
+
+ // Does not change to reload immediately when the mouse is hovered.
+ EXPECT_TRUE([button_ isMouseInside]);
+ [button_ setIsLoading:NO force:NO];
+ EXPECT_TRUE([button_ isMouseInside]);
+ EXPECT_EQ(IDC_STOP, [button_ tag]);
+ EXPECT_TRUE([button_ isMouseInside]);
+
+ // Spin event loop until the timeout passes.
+ NSDate* pastTimeout = [NSDate dateWithTimeIntervalSinceNow:2 * kShortTimeout];
+ [NSApp nextEventMatchingMask:NSAnyEventMask
+ untilDate:pastTimeout
+ inMode:NSDefaultRunLoopMode
+ dequeue:NO];
+
+ // Mouse is still hovered, button is in reload mode. If the mouse
+ // is no longer hovered, see comment at top of function.
+ EXPECT_TRUE([button_ isMouseInside]);
+ EXPECT_EQ(IDC_RELOAD, [button_ tag]);
+}
+
+// Test that pressing stop after reload mode has been requested
+// doesn't forward the stop message.
+TEST_F(ReloadButtonTest, StopAfterReloadSet) {
+ id mock_target = [OCMockObject mockForProtocol:@protocol(TargetActionMock)];
+ [button_ setTarget:mock_target];
+ [button_ setAction:@selector(anAction:)];
+
+ EXPECT_FALSE([button_ isMouseInside]);
+
+ // Get to stop mode.
+ [button_ setIsLoading:YES force:YES];
+ EXPECT_EQ(IDC_STOP, [button_ tag]);
+ EXPECT_TRUE([button_ isEnabled]);
+
+ // Expect the action once.
+ [[mock_target expect] anAction:button_];
+
+ // Clicking in stop mode should send the action and transition to
+ // reload mode.
+ const std::pair<NSEvent*,NSEvent*> click =
+ test_event_utils::MouseClickInView(button_, 1);
+ [NSApp postEvent:click.second atStart:YES];
+ [button_ mouseDown:click.first];
+ EXPECT_EQ(IDC_RELOAD, [button_ tag]);
+ EXPECT_TRUE([button_ isEnabled]);
+
+ // Get back to stop mode.
+ [button_ setIsLoading:YES force:YES];
+ EXPECT_EQ(IDC_STOP, [button_ tag]);
+ EXPECT_TRUE([button_ isEnabled]);
+
+ // If hover prevented reload mode immediately taking effect, clicks should do
+ // nothing, because the button should be disabled.
+ [button_ mouseEntered:nil];
+ EXPECT_TRUE([button_ isMouseInside]);
+ [button_ setIsLoading:NO force:NO];
+ EXPECT_EQ(IDC_STOP, [button_ tag]);
+ EXPECT_FALSE([button_ isEnabled]);
+ [NSApp postEvent:click.second atStart:YES];
+ [button_ mouseDown:click.first];
+ EXPECT_EQ(IDC_STOP, [button_ tag]);
+
+ [button_ setTarget:nil];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/repost_form_warning_mac.h b/chrome/browser/ui/cocoa/repost_form_warning_mac.h
new file mode 100644
index 0000000..a7ca8b2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/repost_form_warning_mac.h
@@ -0,0 +1,40 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_REPOST_FORM_WARNING_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_REPOST_FORM_WARNING_MAC_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_ptr.h"
+#include "chrome/browser/ui/cocoa/constrained_window_mac.h"
+
+class RepostFormWarningController;
+
+// Displays a dialog that warns the user that they are about to resubmit
+// a form. To show the dialog, call the |Create| method. It will open the
+// dialog and then delete itself when the user dismisses the dialog.
+class RepostFormWarningMac : public ConstrainedDialogDelegate {
+ public:
+ // Convenience method that creates a new |RepostFormWarningController| and
+ // then a new |RepostFormWarningMac| from that.
+ static RepostFormWarningMac* Create(NSWindow* parent,
+ TabContents* tab_contents);
+
+ RepostFormWarningMac(NSWindow* parent,
+ RepostFormWarningController* controller);
+
+ // ConstrainedWindowDelegateMacSystemSheet methods:
+ virtual void DeleteDelegate();
+
+ private:
+ virtual ~RepostFormWarningMac();
+
+ scoped_ptr<RepostFormWarningController> controller_;
+
+ DISALLOW_COPY_AND_ASSIGN(RepostFormWarningMac);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_REPOST_FORM_WARNING_MAC_H_
diff --git a/chrome/browser/ui/cocoa/repost_form_warning_mac.mm b/chrome/browser/ui/cocoa/repost_form_warning_mac.mm
new file mode 100644
index 0000000..71f292b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/repost_form_warning_mac.mm
@@ -0,0 +1,82 @@
+// 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 "chrome/browser/ui/cocoa/repost_form_warning_mac.h"
+
+#include "app/l10n_util_mac.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/repost_form_warning_controller.h"
+#include "grit/generated_resources.h"
+
+// The delegate of the NSAlert used to display the dialog. Forwards the alert's
+// completion event to the C++ class |RepostFormWarningController|.
+@interface RepostDelegate : NSObject {
+ RepostFormWarningController* warning_; // weak
+}
+- (id)initWithWarning:(RepostFormWarningController*)warning;
+- (void)alertDidEnd:(NSAlert*)alert
+ returnCode:(int)returnCode
+ contextInfo:(void*)contextInfo;
+@end
+
+@implementation RepostDelegate
+- (id)initWithWarning:(RepostFormWarningController*)warning {
+ if ((self = [super init])) {
+ warning_ = warning;
+ }
+ return self;
+}
+
+- (void)alertDidEnd:(NSAlert*)alert
+ returnCode:(int)returnCode
+ contextInfo:(void*)contextInfo {
+ if (returnCode == NSAlertFirstButtonReturn) {
+ warning_->Continue();
+ } else {
+ warning_->Cancel();
+ }
+}
+@end
+
+RepostFormWarningMac* RepostFormWarningMac::Create(NSWindow* parent,
+ TabContents* tab_contents) {
+ return new RepostFormWarningMac(
+ parent,
+ new RepostFormWarningController(tab_contents));
+}
+
+RepostFormWarningMac::RepostFormWarningMac(
+ NSWindow* parent,
+ RepostFormWarningController* controller)
+ : ConstrainedWindowMacDelegateSystemSheet(
+ [[[RepostDelegate alloc] initWithWarning:controller]
+ autorelease],
+ @selector(alertDidEnd:returnCode:contextInfo:)),
+ controller_(controller) {
+ scoped_nsobject<NSAlert> alert([[NSAlert alloc] init]);
+ [alert setMessageText:
+ l10n_util::GetNSStringWithFixup(IDS_HTTP_POST_WARNING_TITLE)];
+ [alert setInformativeText:
+ l10n_util::GetNSStringWithFixup(IDS_HTTP_POST_WARNING)];
+ [alert addButtonWithTitle:
+ l10n_util::GetNSStringWithFixup(IDS_HTTP_POST_WARNING_RESEND)];
+ [alert addButtonWithTitle:
+ l10n_util::GetNSStringWithFixup(IDS_CANCEL)];
+
+ set_sheet(alert);
+
+ controller->Show(this);
+}
+
+RepostFormWarningMac::~RepostFormWarningMac() {
+ NSWindow* window = [(NSAlert*)sheet() window];
+ if (window && is_sheet_open()) {
+ [NSApp endSheet:window
+ returnCode:NSAlertSecondButtonReturn];
+ }
+}
+
+void RepostFormWarningMac::DeleteDelegate() {
+ delete this;
+}
diff --git a/chrome/browser/ui/cocoa/restart_browser.h b/chrome/browser/ui/cocoa/restart_browser.h
new file mode 100644
index 0000000..27bdd35
--- /dev/null
+++ b/chrome/browser/ui/cocoa/restart_browser.h
@@ -0,0 +1,22 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_RESTART_BROWSER_H_
+#define CHROME_BROWSER_UI_COCOA_RESTART_BROWSER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+// This is a functional match for chrome/browser/views/restart_message_box
+// so any code that needs to ask for a browser restart has something like what
+// the Windows code has.
+namespace restart_browser {
+
+// Puts up an alert telling the user to restart their browser. The alert
+// will be hung off |parent| or global otherise.
+void RequestRestart(NSWindow* parent);
+
+} // namespace restart_browser
+
+#endif // CHROME_BROWSER_UI_COCOA_RESTART_BROWSER_H_
diff --git a/chrome/browser/ui/cocoa/restart_browser.mm b/chrome/browser/ui/cocoa/restart_browser.mm
new file mode 100644
index 0000000..c88715a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/restart_browser.mm
@@ -0,0 +1,86 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/restart_browser.h"
+
+#include "app/l10n_util.h"
+#include "app/l10n_util_mac.h"
+#include "chrome/browser/browser_list.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/common/pref_names.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+#include "grit/app_strings.h"
+
+// Helper to clean up after the notification that the alert was dismissed.
+@interface RestartHelper : NSObject {
+ @private
+ NSAlert* alert_;
+}
+- (NSAlert*)alert;
+- (void)alertDidEnd:(NSAlert*)alert
+ returnCode:(int)returnCode
+ contextInfo:(void*)contextInfo;
+@end
+
+@implementation RestartHelper
+
+- (NSAlert*)alert {
+ alert_ = [[NSAlert alloc] init];
+ return alert_;
+}
+
+- (void)dealloc {
+ [alert_ release];
+ [super dealloc];
+}
+
+- (void)alertDidEnd:(NSAlert*)alert
+ returnCode:(int)returnCode
+ contextInfo:(void*)contextInfo {
+ if (returnCode == NSAlertFirstButtonReturn) {
+ // Nothing to do. User will restart later.
+ } else if (returnCode == NSAlertSecondButtonReturn) {
+ // Set the flag to restore state after the restart.
+ PrefService* pref_service = g_browser_process->local_state();
+ pref_service->SetBoolean(prefs::kRestartLastSessionOnShutdown, true);
+ BrowserList::CloseAllBrowsersAndExit();
+ } else {
+ NOTREACHED();
+ }
+ [self autorelease];
+}
+
+@end
+
+namespace restart_browser {
+
+void RequestRestart(NSWindow* parent) {
+ NSString* title =
+ l10n_util::GetNSStringFWithFixup(IDS_PLEASE_RESTART_BROWSER,
+ l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
+ NSString* text =
+ l10n_util::GetNSStringFWithFixup(IDS_UPDATE_RECOMMENDED,
+ l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
+ NSString* notNowButtin = l10n_util::GetNSStringWithFixup(IDS_NOT_NOW);
+ NSString* restartButton =
+ l10n_util::GetNSStringWithFixup(IDS_RESTART_AND_UPDATE);
+
+ RestartHelper* helper = [[RestartHelper alloc] init];
+
+ NSAlert* alert = [helper alert];
+ [alert setAlertStyle:NSInformationalAlertStyle];
+ [alert setMessageText:title];
+ [alert setInformativeText:text];
+ [alert addButtonWithTitle:notNowButtin];
+ [alert addButtonWithTitle:restartButton];
+
+ [alert beginSheetModalForWindow:parent
+ modalDelegate:helper
+ didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
+ contextInfo:nil];
+}
+
+} // namespace restart_browser
diff --git a/chrome/browser/ui/cocoa/rwhvm_editcommand_helper.h b/chrome/browser/ui/cocoa/rwhvm_editcommand_helper.h
new file mode 100644
index 0000000..92935a4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/rwhvm_editcommand_helper.h
@@ -0,0 +1,72 @@
+// 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_UI_COCOA_RWHVM_EDITCOMMAND_HELPER_H_
+#define CHROME_BROWSER_UI_COCOA_RWHVM_EDITCOMMAND_HELPER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/basictypes.h"
+#include "base/hash_tables.h"
+#include "base/gtest_prod_util.h"
+#include "chrome/browser/renderer_host/render_widget_host_view_mac.h"
+
+// RenderWidgetHostViewMacEditCommandHelper is the real name of this class
+// but that's too long, so we use a shorter version.
+//
+// This class mimics the behavior of WebKit's WebView class in a way that makes
+// sense for Chrome.
+//
+// WebCore has the concept of "core commands", basically named actions such as
+// "Select All" and "Move Cursor Left". The commands are executed using their
+// string value by WebCore.
+//
+// This class is responsible for 2 things:
+// 1. Provide an abstraction to determine the enabled/disabled state of menu
+// items that correspond to edit commands.
+// 2. Hook up a bunch of objc selectors to the RenderWidgetHostViewCocoa object.
+// (note that this is not a misspelling of RenderWidgetHostViewMac, it's in
+// fact a distinct object) When these selectors are called, the relevant
+// edit command is executed in WebCore.
+class RWHVMEditCommandHelper {
+ FRIEND_TEST_ALL_PREFIXES(RWHVMEditCommandHelperTest,
+ TestAddEditingSelectorsToClass);
+ FRIEND_TEST_ALL_PREFIXES(RWHVMEditCommandHelperTest,
+ TestEditingCommandDelivery);
+
+ public:
+ RWHVMEditCommandHelper();
+
+ // Adds editing selectors to the objc class using the objc runtime APIs.
+ // Each selector is connected to a single c method which forwards the message
+ // to WebCore's ExecuteEditCommand() function.
+ // This method is idempotent.
+ // The class passed in must conform to the RenderWidgetHostViewMacOwner
+ // protocol.
+ void AddEditingSelectorsToClass(Class klass);
+
+ // Is a given menu item currently enabled?
+ // SEL - the objc selector currently associated with an NSMenuItem.
+ // owner - An object we can retrieve a RenderWidgetHostViewMac from to
+ // determine the command states.
+ bool IsMenuItemEnabled(SEL item_action,
+ id<RenderWidgetHostViewMacOwner> owner);
+
+ // Converts an editing selector into a command name that can be sent to
+ // webkit.
+ static NSString* CommandNameForSelector(SEL selector);
+
+ protected:
+ // Gets a list of all the selectors that AddEditingSelectorsToClass adds to
+ // the aforementioned class.
+ // returns an array of NSStrings WITHOUT the trailing ':'s.
+ NSArray* GetEditSelectorNames();
+
+ private:
+ base::hash_set<std::string> edit_command_set_;
+ DISALLOW_COPY_AND_ASSIGN(RWHVMEditCommandHelper);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_RWHVM_EDITCOMMAND_HELPER_H_
diff --git a/chrome/browser/ui/cocoa/rwhvm_editcommand_helper.mm b/chrome/browser/ui/cocoa/rwhvm_editcommand_helper.mm
new file mode 100644
index 0000000..0aec09e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/rwhvm_editcommand_helper.mm
@@ -0,0 +1,227 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/rwhvm_editcommand_helper.h"
+
+#import <objc/runtime.h>
+
+#include "chrome/browser/renderer_host/render_widget_host.h"
+#import "chrome/browser/renderer_host/render_widget_host_view_mac.h"
+
+namespace {
+// The names of all the objc selectors w/o ':'s added to an object by
+// AddEditingSelectorsToClass().
+//
+// This needs to be kept in Sync with WEB_COMMAND list in the WebKit tree at:
+// WebKit/mac/WebView/WebHTMLView.mm .
+const char* kEditCommands[] = {
+ "alignCenter",
+ "alignJustified",
+ "alignLeft",
+ "alignRight",
+ "copy",
+ "cut",
+ "delete",
+ "deleteBackward",
+ "deleteBackwardByDecomposingPreviousCharacter",
+ "deleteForward",
+ "deleteToBeginningOfLine",
+ "deleteToBeginningOfParagraph",
+ "deleteToEndOfLine",
+ "deleteToEndOfParagraph",
+ "deleteToMark",
+ "deleteWordBackward",
+ "deleteWordForward",
+ "ignoreSpelling",
+ "indent",
+ "insertBacktab",
+ "insertLineBreak",
+ "insertNewline",
+ "insertNewlineIgnoringFieldEditor",
+ "insertParagraphSeparator",
+ "insertTab",
+ "insertTabIgnoringFieldEditor",
+ "makeTextWritingDirectionLeftToRight",
+ "makeTextWritingDirectionNatural",
+ "makeTextWritingDirectionRightToLeft",
+ "moveBackward",
+ "moveBackwardAndModifySelection",
+ "moveDown",
+ "moveDownAndModifySelection",
+ "moveForward",
+ "moveForwardAndModifySelection",
+ "moveLeft",
+ "moveLeftAndModifySelection",
+ "moveParagraphBackwardAndModifySelection",
+ "moveParagraphForwardAndModifySelection",
+ "moveRight",
+ "moveRightAndModifySelection",
+ "moveToBeginningOfDocument",
+ "moveToBeginningOfDocumentAndModifySelection",
+ "moveToBeginningOfLine",
+ "moveToBeginningOfLineAndModifySelection",
+ "moveToBeginningOfParagraph",
+ "moveToBeginningOfParagraphAndModifySelection",
+ "moveToBeginningOfSentence",
+ "moveToBeginningOfSentenceAndModifySelection",
+ "moveToEndOfDocument",
+ "moveToEndOfDocumentAndModifySelection",
+ "moveToEndOfLine",
+ "moveToEndOfLineAndModifySelection",
+ "moveToEndOfParagraph",
+ "moveToEndOfParagraphAndModifySelection",
+ "moveToEndOfSentence",
+ "moveToEndOfSentenceAndModifySelection",
+ "moveUp",
+ "moveUpAndModifySelection",
+ "moveWordBackward",
+ "moveWordBackwardAndModifySelection",
+ "moveWordForward",
+ "moveWordForwardAndModifySelection",
+ "moveWordLeft",
+ "moveWordLeftAndModifySelection",
+ "moveWordRight",
+ "moveWordRightAndModifySelection",
+ "outdent",
+ "pageDown",
+ "pageDownAndModifySelection",
+ "pageUp",
+ "pageUpAndModifySelection",
+ "selectAll",
+ "selectLine",
+ "selectParagraph",
+ "selectSentence",
+ "selectToMark",
+ "selectWord",
+ "setMark",
+ "showGuessPanel",
+ "subscript",
+ "superscript",
+ "swapWithMark",
+ "transpose",
+ "underline",
+ "unscript",
+ "yank",
+ "yankAndSelect"};
+
+
+// This function is installed via the objc runtime as the implementation of all
+// the various editing selectors.
+// The objc runtime hookup occurs in
+// RWHVMEditCommandHelper::AddEditingSelectorsToClass().
+//
+// self - the object we're attached to; it must implement the
+// RenderWidgetHostViewMacOwner protocol.
+// _cmd - the selector that fired.
+// sender - the id of the object that sent the message.
+//
+// The selector is translated into an edit comand and then forwarded down the
+// pipeline to WebCore.
+// The route the message takes is:
+// RenderWidgetHostViewMac -> RenderViewHost ->
+// | IPC | ->
+// RenderView -> currently focused WebFrame.
+// The WebFrame is in the Chrome glue layer and forwards the message to WebCore.
+void EditCommandImp(id self, SEL _cmd, id sender) {
+ // Make sure |self| is the right type.
+ DCHECK([self conformsToProtocol:@protocol(RenderWidgetHostViewMacOwner)]);
+
+ // SEL -> command name string.
+ NSString* command_name_ns =
+ RWHVMEditCommandHelper::CommandNameForSelector(_cmd);
+ std::string edit_command([command_name_ns UTF8String]);
+
+ // Forward the edit command string down the pipeline.
+ RenderWidgetHostViewMac* rwhv = [(id<RenderWidgetHostViewMacOwner>)self
+ renderWidgetHostViewMac];
+ DCHECK(rwhv);
+
+ // The second parameter is the core command value which isn't used here.
+ rwhv->GetRenderWidgetHost()->ForwardEditCommand(edit_command, "");
+}
+
+} // namespace
+
+// Maps an objc-selector to a core command name.
+//
+// Returns the core command name (which is the selector name with the trailing
+// ':' stripped in most cases).
+//
+// Adapted from a function by the same name in
+// WebKit/mac/WebView/WebHTMLView.mm .
+// Capitalized names are returned from this function, but that's simply
+// matching WebHTMLView.mm.
+NSString* RWHVMEditCommandHelper::CommandNameForSelector(SEL selector) {
+ if (selector == @selector(insertParagraphSeparator:) ||
+ selector == @selector(insertNewlineIgnoringFieldEditor:))
+ return @"InsertNewline";
+ if (selector == @selector(insertTabIgnoringFieldEditor:))
+ return @"InsertTab";
+ if (selector == @selector(pageDown:))
+ return @"MovePageDown";
+ if (selector == @selector(pageDownAndModifySelection:))
+ return @"MovePageDownAndModifySelection";
+ if (selector == @selector(pageUp:))
+ return @"MovePageUp";
+ if (selector == @selector(pageUpAndModifySelection:))
+ return @"MovePageUpAndModifySelection";
+
+ // Remove the trailing colon.
+ NSString* selector_str = NSStringFromSelector(selector);
+ int selector_len = [selector_str length];
+ return [selector_str substringToIndex:selector_len - 1];
+}
+
+RWHVMEditCommandHelper::RWHVMEditCommandHelper() {
+ for (size_t i = 0; i < arraysize(kEditCommands); ++i) {
+ edit_command_set_.insert(kEditCommands[i]);
+ }
+}
+
+// Dynamically adds Selectors to the aformentioned class.
+void RWHVMEditCommandHelper::AddEditingSelectorsToClass(Class klass) {
+ for (size_t i = 0; i < arraysize(kEditCommands); ++i) {
+ // Append trailing ':' to command name to get selector name.
+ NSString* sel_str = [NSString stringWithFormat: @"%s:", kEditCommands[i]];
+
+ SEL edit_selector = NSSelectorFromString(sel_str);
+ // May want to use @encode() for the last parameter to this method.
+ // If class_addMethod fails we assume that all the editing selectors where
+ // added to the class.
+ // If a certain class already implements a method then class_addMethod
+ // returns NO, which we can safely ignore.
+ class_addMethod(klass, edit_selector, (IMP)EditCommandImp, "v@:@");
+ }
+}
+
+bool RWHVMEditCommandHelper::IsMenuItemEnabled(SEL item_action,
+ id<RenderWidgetHostViewMacOwner> owner) {
+ const char* selector_name = sel_getName(item_action);
+ // TODO(jeremy): The final form of this function will check state
+ // associated with the Browser.
+
+ // For now just mark all edit commands as enabled.
+ NSString* selector_name_ns = [NSString stringWithUTF8String:selector_name];
+
+ // Remove trailing ':'
+ size_t str_len = [selector_name_ns length];
+ selector_name_ns = [selector_name_ns substringToIndex:str_len - 1];
+ std::string edit_command_name([selector_name_ns UTF8String]);
+
+ // search for presence in set and return.
+ bool ret = edit_command_set_.find(edit_command_name) !=
+ edit_command_set_.end();
+ return ret;
+}
+
+NSArray* RWHVMEditCommandHelper::GetEditSelectorNames() {
+ size_t num_edit_commands = arraysize(kEditCommands);
+ NSMutableArray* ret = [NSMutableArray arrayWithCapacity:num_edit_commands];
+
+ for (size_t i = 0; i < num_edit_commands; ++i) {
+ [ret addObject:[NSString stringWithUTF8String:kEditCommands[i]]];
+ }
+
+ return ret;
+}
diff --git a/chrome/browser/ui/cocoa/rwhvm_editcommand_helper_unittest.mm b/chrome/browser/ui/cocoa/rwhvm_editcommand_helper_unittest.mm
new file mode 100644
index 0000000..776c400
--- /dev/null
+++ b/chrome/browser/ui/cocoa/rwhvm_editcommand_helper_unittest.mm
@@ -0,0 +1,172 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/rwhvm_editcommand_helper.h"
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/renderer_host/mock_render_process_host.h"
+#include "chrome/browser/renderer_host/render_widget_host.h"
+#include "chrome/test/testing_profile.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+class RWHVMEditCommandHelperTest : public PlatformTest {
+};
+
+// Bare bones obj-c class for testing purposes.
+@interface RWHVMEditCommandHelperTestClass : NSObject
+@end
+
+@implementation RWHVMEditCommandHelperTestClass
+@end
+
+// Class that owns a RenderWidgetHostViewMac.
+@interface RenderWidgetHostViewMacOwner :
+ NSObject<RenderWidgetHostViewMacOwner> {
+ RenderWidgetHostViewMac* rwhvm_;
+}
+
+- (id) initWithRenderWidgetHostViewMac:(RenderWidgetHostViewMac*)rwhvm;
+@end
+
+@implementation RenderWidgetHostViewMacOwner
+
+- (id)initWithRenderWidgetHostViewMac:(RenderWidgetHostViewMac*)rwhvm {
+ if ((self = [super init])) {
+ rwhvm_ = rwhvm;
+ }
+ return self;
+}
+
+- (RenderWidgetHostViewMac*)renderWidgetHostViewMac {
+ return rwhvm_;
+}
+
+@end
+
+
+namespace {
+ // Returns true if all the edit command names in the array are present
+ // in test_obj.
+ // edit_commands is a list of NSStrings, selector names are formed by
+ // appending a trailing ':' to the string.
+ bool CheckObjectRespondsToEditCommands(NSArray* edit_commands, id test_obj) {
+ for (NSString* edit_command_name in edit_commands) {
+ NSString* sel_str = [edit_command_name stringByAppendingString:@":"];
+ if (![test_obj respondsToSelector:NSSelectorFromString(sel_str)]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+} // namespace
+
+// Create a Mock RenderWidget
+class MockRenderWidgetHostEditCommandCounter : public RenderWidgetHost {
+ public:
+ MockRenderWidgetHostEditCommandCounter(RenderProcessHost* process,
+ int routing_id) :
+ RenderWidgetHost(process, routing_id) {}
+
+ MOCK_METHOD2(ForwardEditCommand, void(const std::string&,
+ const std::string&));
+};
+
+
+// Tests that editing commands make it through the pipeline all the way to
+// RenderWidgetHost.
+TEST_F(RWHVMEditCommandHelperTest, TestEditingCommandDelivery) {
+ RWHVMEditCommandHelper helper;
+ NSArray* edit_command_strings = helper.GetEditSelectorNames();
+
+ // Set up a mock render widget and set expectations.
+ MessageLoopForUI message_loop;
+ TestingProfile profile;
+ MockRenderProcessHost mock_process(&profile);
+ MockRenderWidgetHostEditCommandCounter mock_render_widget(&mock_process, 0);
+
+ size_t num_edit_commands = [edit_command_strings count];
+ EXPECT_CALL(mock_render_widget,
+ ForwardEditCommand(testing::_, testing::_)).Times(num_edit_commands);
+
+// TODO(jeremy): Figure this out and reenable this test.
+// For some bizarre reason this code doesn't work, running the code in the
+// debugger confirms that the function is called with the correct parameters
+// however gmock appears not to be able to match up parameters correctly.
+// Disable for now till we can figure this out.
+#if 0
+ // Tell Mock object that we expect to recieve each edit command once.
+ std::string empty_str;
+ for (NSString* edit_command_name in edit_command_strings) {
+ std::string command([edit_command_name UTF8String]);
+ EXPECT_CALL(mock_render_widget,
+ ForwardEditCommand(command, empty_str)).Times(1);
+ }
+#endif // 0
+
+ // RenderWidgetHostViewMac self destructs (RenderWidgetHostViewMacCocoa
+ // takes ownership) so no need to delete it ourselves.
+ RenderWidgetHostViewMac* rwhvm = new RenderWidgetHostViewMac(
+ &mock_render_widget);
+
+ RenderWidgetHostViewMacOwner* rwhwvm_owner =
+ [[[RenderWidgetHostViewMacOwner alloc]
+ initWithRenderWidgetHostViewMac:rwhvm] autorelease];
+
+ helper.AddEditingSelectorsToClass([rwhwvm_owner class]);
+
+ for (NSString* edit_command_name in edit_command_strings) {
+ NSString* sel_str = [edit_command_name stringByAppendingString:@":"];
+ [rwhwvm_owner performSelector:NSSelectorFromString(sel_str) withObject:nil];
+ }
+}
+
+// Test RWHVMEditCommandHelper::AddEditingSelectorsToClass
+TEST_F(RWHVMEditCommandHelperTest, TestAddEditingSelectorsToClass) {
+ RWHVMEditCommandHelper helper;
+ NSArray* edit_command_strings = helper.GetEditSelectorNames();
+ ASSERT_GT([edit_command_strings count], 0U);
+
+ // Create a class instance and add methods to the class.
+ RWHVMEditCommandHelperTestClass* test_obj =
+ [[[RWHVMEditCommandHelperTestClass alloc] init] autorelease];
+
+ // Check that edit commands aren't already attached to the object.
+ ASSERT_FALSE(CheckObjectRespondsToEditCommands(edit_command_strings,
+ test_obj));
+
+ helper.AddEditingSelectorsToClass([test_obj class]);
+
+ // Check that all edit commands where added.
+ ASSERT_TRUE(CheckObjectRespondsToEditCommands(edit_command_strings,
+ test_obj));
+
+ // AddEditingSelectorsToClass() should be idempotent.
+ helper.AddEditingSelectorsToClass([test_obj class]);
+
+ // Check that all edit commands are still there.
+ ASSERT_TRUE(CheckObjectRespondsToEditCommands(edit_command_strings,
+ test_obj));
+}
+
+// Test RWHVMEditCommandHelper::IsMenuItemEnabled.
+TEST_F(RWHVMEditCommandHelperTest, TestMenuItemEnabling) {
+ RWHVMEditCommandHelper helper;
+ RenderWidgetHostViewMacOwner* rwhvm_owner =
+ [[[RenderWidgetHostViewMacOwner alloc] init] autorelease];
+
+ // The select all menu should always be enabled.
+ SEL select_all = NSSelectorFromString(@"selectAll:");
+ ASSERT_TRUE(helper.IsMenuItemEnabled(select_all, rwhvm_owner));
+
+ // Random selectors should be enabled by the function.
+ SEL garbage_selector = NSSelectorFromString(@"randomGarbageSelector:");
+ ASSERT_FALSE(helper.IsMenuItemEnabled(garbage_selector, rwhvm_owner));
+
+ // TODO(jeremy): Currently IsMenuItemEnabled just returns true for all edit
+ // selectors. Once we go past that we should do more extensive testing here.
+}
diff --git a/chrome/browser/ui/cocoa/sad_tab_controller.h b/chrome/browser/ui/cocoa/sad_tab_controller.h
new file mode 100644
index 0000000..35e9aaf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/sad_tab_controller.h
@@ -0,0 +1,33 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_SAD_TAB_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_SAD_TAB_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+class TabContents;
+
+// A controller class that manages the SadTabView (aka "Aw Snap" or crash page).
+@interface SadTabController : NSViewController {
+ @private
+ TabContents* tabContents_; // Weak reference.
+}
+
+// Designated initializer is initWithTabContents.
+- (id)initWithTabContents:(TabContents*)someTabContents
+ superview:(NSView*)superview;
+
+// This action just calls the NSApp sendAction to get it into the standard
+// Cocoa action processing.
+- (IBAction)openLearnMoreAboutCrashLink:(id)sender;
+
+// Returns a weak reference to the TabContents whose TabContentsView created
+// this SadTabController.
+- (TabContents*)tabContents;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_SAD_TAB_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/sad_tab_controller.mm b/chrome/browser/ui/cocoa/sad_tab_controller.mm
new file mode 100644
index 0000000..ba0b102
--- /dev/null
+++ b/chrome/browser/ui/cocoa/sad_tab_controller.mm
@@ -0,0 +1,48 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/sad_tab_controller.h"
+
+#include "base/mac_util.h"
+#import "chrome/browser/ui/cocoa/sad_tab_view.h"
+
+@implementation SadTabController
+
+- (id)initWithTabContents:(TabContents*)someTabContents
+ superview:(NSView*)superview {
+ if ((self = [super initWithNibName:@"SadTab"
+ bundle:mac_util::MainAppBundle()])) {
+ tabContents_ = someTabContents;
+
+ NSView* view = [self view];
+ [superview addSubview:view];
+ [view setFrame:[superview bounds]];
+ }
+
+ return self;
+}
+
+- (void)awakeFromNib {
+ // If tab_contents_ is nil, ask view to remove link.
+ if (!tabContents_) {
+ SadTabView* sad_view = static_cast<SadTabView*>([self view]);
+ [sad_view removeLinkButton];
+ }
+}
+
+- (void)dealloc {
+ [[self view] removeFromSuperview];
+ [super dealloc];
+}
+
+- (TabContents*)tabContents {
+ return tabContents_;
+}
+
+- (void)openLearnMoreAboutCrashLink:(id)sender {
+ // Send the action up through the responder chain.
+ [NSApp sendAction:@selector(openLearnMoreAboutCrashLink:) to:nil from:self];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/sad_tab_controller_unittest.mm b/chrome/browser/ui/cocoa/sad_tab_controller_unittest.mm
new file mode 100644
index 0000000..15839a8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/sad_tab_controller_unittest.mm
@@ -0,0 +1,113 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/debug/debugger.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/sad_tab_controller.h"
+#import "chrome/browser/ui/cocoa/sad_tab_view.h"
+#include "chrome/browser/renderer_host/test/test_render_view_host.h"
+#include "chrome/browser/tab_contents/test_tab_contents.h"
+#include "chrome/test/testing_profile.h"
+
+@interface SadTabView (ExposedForTesting)
+// Implementation is below.
+- (NSButton*)linkButton;
+@end
+
+@implementation SadTabView (ExposedForTesting)
+- (NSButton*)linkButton {
+ return linkButton_;
+}
+@end
+
+namespace {
+
+class SadTabControllerTest : public RenderViewHostTestHarness {
+ public:
+ SadTabControllerTest() : test_window_(nil) {
+ link_clicked_ = false;
+ }
+
+ virtual void SetUp() {
+ RenderViewHostTestHarness::SetUp();
+ // Inherting from RenderViewHostTestHarness means we can't inherit from
+ // from CocoaTest, so do a bootstrap and create test window.
+ CocoaTest::BootstrapCocoa();
+ test_window_ = [[CocoaTestHelperWindow alloc] init];
+ if (base::debug::BeingDebugged()) {
+ [test_window_ orderFront:nil];
+ } else {
+ [test_window_ orderBack:nil];
+ }
+ }
+
+ virtual void TearDown() {
+ [test_window_ close];
+ test_window_ = nil;
+ RenderViewHostTestHarness::TearDown();
+ }
+
+ // Creates the controller and adds its view to contents, caller has ownership.
+ SadTabController* CreateController() {
+ NSView* contentView = [test_window_ contentView];
+ SadTabController* controller =
+ [[SadTabController alloc] initWithTabContents:contents()
+ superview:contentView];
+ EXPECT_TRUE(controller);
+ NSView* view = [controller view];
+ EXPECT_TRUE(view);
+
+ return controller;
+ }
+
+ NSButton* GetLinkButton(SadTabController* controller) {
+ SadTabView* view = static_cast<SadTabView*>([controller view]);
+ return ([view linkButton]);
+ }
+
+ static bool link_clicked_;
+ CocoaTestHelperWindow* test_window_;
+};
+
+// static
+bool SadTabControllerTest::link_clicked_;
+
+TEST_F(SadTabControllerTest, WithTabContents) {
+ scoped_nsobject<SadTabController> controller(CreateController());
+ EXPECT_TRUE(controller);
+ NSButton* link = GetLinkButton(controller);
+ EXPECT_TRUE(link);
+}
+
+TEST_F(SadTabControllerTest, WithoutTabContents) {
+ contents_.reset();
+ scoped_nsobject<SadTabController> controller(CreateController());
+ EXPECT_TRUE(controller);
+ NSButton* link = GetLinkButton(controller);
+ EXPECT_FALSE(link);
+}
+
+TEST_F(SadTabControllerTest, ClickOnLink) {
+ scoped_nsobject<SadTabController> controller(CreateController());
+ NSButton* link = GetLinkButton(controller);
+ EXPECT_TRUE(link);
+ EXPECT_FALSE(link_clicked_);
+ [link performClick:link];
+ EXPECT_TRUE(link_clicked_);
+}
+
+} // namespace
+
+@implementation NSApplication (SadTabControllerUnitTest)
+// Add handler for the openLearnMoreAboutCrashLink: action to NSApp for testing
+// purposes. Normally this would be sent up the responder tree correctly, but
+// since tests run in the background, key window and main window are never set
+// on NSApplication. Adding it to NSApplication directly removes the need for
+// worrying about what the current window with focus is.
+- (void)openLearnMoreAboutCrashLink:(id)sender {
+ SadTabControllerTest::link_clicked_ = true;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/sad_tab_view.h b/chrome/browser/ui/cocoa/sad_tab_view.h
new file mode 100644
index 0000000..0f304eb
--- /dev/null
+++ b/chrome/browser/ui/cocoa/sad_tab_view.h
@@ -0,0 +1,36 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_SAD_TAB_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_SAD_TAB_VIEW_H_
+#pragma once
+
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/ui/cocoa/base_view.h"
+
+#import <Cocoa/Cocoa.h>
+
+@class HyperlinkButtonCell;
+
+// A view that displays the "sad tab" (aka crash page).
+@interface SadTabView : BaseView {
+ @private
+ IBOutlet NSImageView* image_;
+ IBOutlet NSTextField* title_;
+ IBOutlet NSTextField* message_;
+ IBOutlet NSButton* linkButton_;
+ IBOutlet HyperlinkButtonCell* linkCell_;
+
+ scoped_nsobject<NSColor> backgroundColor_;
+ NSSize messageSize_;
+}
+
+// Designated initializer is -initWithFrame: .
+
+// Called by SadTabController to remove link button.
+- (void)removeLinkButton;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_SAD_TAB_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/sad_tab_view.mm b/chrome/browser/ui/cocoa/sad_tab_view.mm
new file mode 100644
index 0000000..2c9f9e2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/sad_tab_view.mm
@@ -0,0 +1,127 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/sad_tab_view.h"
+
+#include "app/resource_bundle.h"
+#include "base/logging.h"
+#import "chrome/browser/ui/cocoa/hyperlink_button_cell.h"
+#include "grit/theme_resources.h"
+#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+// Offset above vertical middle of page where contents of page start.
+static const CGFloat kSadTabOffset = -64;
+// Padding between icon and title.
+static const CGFloat kIconTitleSpacing = 20;
+// Padding between title and message.
+static const CGFloat kTitleMessageSpacing = 15;
+// Padding between message and link.
+static const CGFloat kMessageLinkSpacing = 15;
+// Paddings on left and right of page.
+static const CGFloat kTabHorzMargin = 13;
+
+@implementation SadTabView
+
+- (void)awakeFromNib {
+ // Load resource for image and set it.
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ NSImage* image = rb.GetNativeImageNamed(IDR_SAD_TAB);
+ DCHECK(image);
+ [image_ setImage:image];
+
+ // Set font for title.
+ NSFont* titleFont = [NSFont boldSystemFontOfSize:[NSFont systemFontSize]];
+ [title_ setFont:titleFont];
+
+ // Set font for message.
+ NSFont* messageFont = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
+ [message_ setFont:messageFont];
+
+ // If necessary, set font and color for link.
+ if (linkButton_) {
+ [linkButton_ setFont:messageFont];
+ [linkCell_ setTextColor:[NSColor whiteColor]];
+ }
+
+ // Initialize background color.
+ NSColor* backgroundColor = [[NSColor colorWithCalibratedRed:(35.0f/255.0f)
+ green:(48.0f/255.0f)
+ blue:(64.0f/255.0f)
+ alpha:1.0] retain];
+ backgroundColor_.reset(backgroundColor);
+}
+
+- (void)drawRect:(NSRect)dirtyRect {
+ // Paint background.
+ [backgroundColor_ set];
+ NSRectFill(dirtyRect);
+}
+
+- (void)resizeSubviewsWithOldSize:(NSSize)oldSize {
+ NSRect newBounds = [self bounds];
+ CGFloat maxWidth = NSWidth(newBounds) - (kTabHorzMargin * 2);
+ BOOL callSizeToFit = (messageSize_.width == 0);
+
+ // Set new frame origin for image.
+ NSRect iconFrame = [image_ frame];
+ CGFloat iconX = (maxWidth - NSWidth(iconFrame)) / 2;
+ CGFloat iconY =
+ MIN(((NSHeight(newBounds) - NSHeight(iconFrame)) / 2) - kSadTabOffset,
+ NSHeight(newBounds) - NSHeight(iconFrame));
+ iconX = floor(iconX);
+ iconY = floor(iconY);
+ [image_ setFrameOrigin:NSMakePoint(iconX, iconY)];
+
+ // Set new frame origin for title.
+ if (callSizeToFit)
+ [title_ sizeToFit];
+ NSRect titleFrame = [title_ frame];
+ CGFloat titleX = (maxWidth - NSWidth(titleFrame)) / 2;
+ CGFloat titleY = iconY - kIconTitleSpacing - NSHeight(titleFrame);
+ [title_ setFrameOrigin:NSMakePoint(titleX, titleY)];
+
+ // Set new frame for message, wrapping or unwrapping the text if necessary.
+ if (callSizeToFit) {
+ [message_ sizeToFit];
+ messageSize_ = [message_ frame].size;
+ }
+ NSRect messageFrame = [message_ frame];
+ if (messageSize_.width > maxWidth) { // Need to wrap message.
+ [message_ setFrameSize:NSMakeSize(maxWidth, messageSize_.height)];
+ CGFloat heightChange =
+ [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:message_];
+ messageFrame.size.width = maxWidth;
+ messageFrame.size.height = messageSize_.height + heightChange;
+ messageFrame.origin.x = kTabHorzMargin;
+ } else {
+ if (!callSizeToFit) {
+ [message_ sizeToFit];
+ messageFrame = [message_ frame];
+ }
+ messageFrame.origin.x = (maxWidth - NSWidth(messageFrame)) / 2;
+ }
+ messageFrame.origin.y =
+ titleY - kTitleMessageSpacing - NSHeight(messageFrame);
+ [message_ setFrame:messageFrame];
+
+ if (linkButton_) {
+ if (callSizeToFit)
+ [linkButton_ sizeToFit];
+ // Set new frame origin for link.
+ NSRect linkFrame = [linkButton_ frame];
+ CGFloat linkX = (maxWidth - NSWidth(linkFrame)) / 2;
+ CGFloat linkY =
+ NSMinY(messageFrame) - kMessageLinkSpacing - NSHeight(linkFrame);
+ [linkButton_ setFrameOrigin:NSMakePoint(linkX, linkY)];
+ }
+}
+
+- (void)removeLinkButton {
+ if (linkButton_) {
+ [linkButton_ removeFromSuperview];
+ linkButton_ = nil;
+ }
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/sad_tab_view_unittest.mm b/chrome/browser/ui/cocoa/sad_tab_view_unittest.mm
new file mode 100644
index 0000000..2321dd3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/sad_tab_view_unittest.mm
@@ -0,0 +1,25 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/sad_tab_view.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+
+namespace {
+
+class SadTabViewTest : public CocoaTest {
+ public:
+ SadTabViewTest() {
+ NSRect content_frame = [[test_window() contentView] frame];
+ scoped_nsobject<SadTabView> view([[SadTabView alloc]
+ initWithFrame:content_frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ SadTabView* view_; // Weak. Owned by the view hierarchy.
+};
+
+TEST_VIEW(SadTabViewTest, view_);
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/scoped_authorizationref.h b/chrome/browser/ui/cocoa/scoped_authorizationref.h
new file mode 100644
index 0000000..ae7edb3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/scoped_authorizationref.h
@@ -0,0 +1,80 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_SCOPED_AUTHORIZATIONREF_H_
+#define CHROME_BROWSER_UI_COCOA_SCOPED_AUTHORIZATIONREF_H_
+#pragma once
+
+#include <Security/Authorization.h>
+
+#include "base/basictypes.h"
+#include "base/compiler_specific.h"
+
+// scoped_AuthorizationRef maintains ownership of an AuthorizationRef. It is
+// patterned after the scoped_ptr interface.
+
+class scoped_AuthorizationRef {
+ public:
+ explicit scoped_AuthorizationRef(AuthorizationRef authorization = NULL)
+ : authorization_(authorization) {
+ }
+
+ ~scoped_AuthorizationRef() {
+ if (authorization_) {
+ AuthorizationFree(authorization_, kAuthorizationFlagDestroyRights);
+ }
+ }
+
+ void reset(AuthorizationRef authorization = NULL) {
+ if (authorization_ != authorization) {
+ if (authorization_) {
+ AuthorizationFree(authorization_, kAuthorizationFlagDestroyRights);
+ }
+ authorization_ = authorization;
+ }
+ }
+
+ bool operator==(AuthorizationRef that) const {
+ return authorization_ == that;
+ }
+
+ bool operator!=(AuthorizationRef that) const {
+ return authorization_ != that;
+ }
+
+ operator AuthorizationRef() const {
+ return authorization_;
+ }
+
+ AuthorizationRef* operator&() {
+ return &authorization_;
+ }
+
+ AuthorizationRef get() const {
+ return authorization_;
+ }
+
+ void swap(scoped_AuthorizationRef& that) {
+ AuthorizationRef temp = that.authorization_;
+ that.authorization_ = authorization_;
+ authorization_ = temp;
+ }
+
+ // scoped_AuthorizationRef::release() is like scoped_ptr<>::release. It is
+ // NOT a wrapper for AuthorizationFree(). To force a
+ // scoped_AuthorizationRef object to call AuthorizationFree(), use
+ // scoped_AuthorizaitonRef::reset().
+ AuthorizationRef release() WARN_UNUSED_RESULT {
+ AuthorizationRef temp = authorization_;
+ authorization_ = NULL;
+ return temp;
+ }
+
+ private:
+ AuthorizationRef authorization_;
+
+ DISALLOW_COPY_AND_ASSIGN(scoped_AuthorizationRef);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_SCOPED_AUTHORIZATIONREF_H_
diff --git a/chrome/browser/ui/cocoa/search_engine_dialog_controller.h b/chrome/browser/ui/cocoa/search_engine_dialog_controller.h
new file mode 100644
index 0000000..0cc06f2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/search_engine_dialog_controller.h
@@ -0,0 +1,46 @@
+// 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 <vector>
+
+#import "base/ref_counted.h"
+#import "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+
+class Profile;
+class SearchEngineDialogControllerBridge;
+class TemplateURL;
+class TemplateURLModel;
+
+// Class that acts as a controller for the search engine choice dialog.
+@interface SearchEngineDialogController : NSWindowController {
+ @private
+ // Our current profile.
+ Profile* profile_;
+
+ // If logos are to be displayed in random order. Used for UX testing.
+ bool randomize_;
+
+ // Owned by the profile_.
+ TemplateURLModel* searchEnginesModel_;
+
+ // Bridge to the C++ world.
+ scoped_refptr<SearchEngineDialogControllerBridge> bridge_;
+
+ // Offered search engine choices.
+ std::vector<const TemplateURL*> choices_;
+
+ IBOutlet NSImageView* headerImageView_;
+ IBOutlet NSView* searchEngineView_;
+}
+
+@property(assign, nonatomic) Profile* profile;
+@property(assign, nonatomic) bool randomize;
+
+// Properties for bindings.
+@property(readonly) NSFont* mainLabelFont;
+
+@end
diff --git a/chrome/browser/ui/cocoa/search_engine_dialog_controller.mm b/chrome/browser/ui/cocoa/search_engine_dialog_controller.mm
new file mode 100644
index 0000000..6cf7911
--- /dev/null
+++ b/chrome/browser/ui/cocoa/search_engine_dialog_controller.mm
@@ -0,0 +1,285 @@
+// 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/ui/cocoa/search_engine_dialog_controller.h"
+
+#include <algorithm>
+
+#include "app/l10n_util_mac.h"
+#include "app/resource_bundle.h"
+#include "base/mac_util.h"
+#include "base/nsimage_cache_mac.h"
+#include "base/sys_string_conversions.h"
+#include "base/time.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/search_engines/template_url.h"
+#include "chrome/browser/search_engines/template_url_model.h"
+#include "chrome/browser/search_engines/template_url_model_observer.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+// Horizontal spacing between search engine choices.
+const int kSearchEngineSpacing = 20;
+
+// Vertical spacing between the search engine logo and the button underneath.
+const int kLogoButtonSpacing = 10;
+
+// Width of a label used in place of a logo.
+const int kLogoLabelWidth = 170;
+
+// Height of a label used in place of a logo.
+const int kLogoLabelHeight = 25;
+
+@interface SearchEngineDialogController (Private)
+- (void)onTemplateURLModelChanged;
+- (void)buildSearchEngineView;
+- (NSView*)viewForSearchEngine:(const TemplateURL*)engine
+ atIndex:(size_t)index;
+- (IBAction)searchEngineSelected:(id)sender;
+@end
+
+class SearchEngineDialogControllerBridge :
+ public base::RefCounted<SearchEngineDialogControllerBridge>,
+ public TemplateURLModelObserver {
+ public:
+ SearchEngineDialogControllerBridge(SearchEngineDialogController* controller);
+
+ // TemplateURLModelObserver
+ virtual void OnTemplateURLModelChanged();
+
+ private:
+ SearchEngineDialogController* controller_;
+};
+
+SearchEngineDialogControllerBridge::SearchEngineDialogControllerBridge(
+ SearchEngineDialogController* controller) : controller_(controller) {
+}
+
+void SearchEngineDialogControllerBridge::OnTemplateURLModelChanged() {
+ [controller_ onTemplateURLModelChanged];
+ MessageLoop::current()->QuitNow();
+}
+
+@implementation SearchEngineDialogController
+
+@synthesize profile = profile_;
+@synthesize randomize = randomize_;
+
+- (id)init {
+ NSString* nibpath =
+ [mac_util::MainAppBundle() pathForResource:@"SearchEngineDialog"
+ ofType:@"nib"];
+ self = [super initWithWindowNibPath:nibpath owner:self];
+ if (self != nil) {
+ bridge_ = new SearchEngineDialogControllerBridge(self);
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [super dealloc];
+}
+
+- (IBAction)showWindow:(id)sender {
+ searchEnginesModel_ = profile_->GetTemplateURLModel();
+ searchEnginesModel_->AddObserver(bridge_.get());
+
+ if (searchEnginesModel_->loaded()) {
+ MessageLoop::current()->PostTask(
+ FROM_HERE,
+ NewRunnableMethod(
+ bridge_.get(),
+ &SearchEngineDialogControllerBridge::OnTemplateURLModelChanged));
+ } else {
+ searchEnginesModel_->Load();
+ }
+ MessageLoop::current()->Run();
+}
+
+- (void)onTemplateURLModelChanged {
+ searchEnginesModel_->RemoveObserver(bridge_.get());
+
+ // Add the search engines in the search_engines_model_ to the buttons list.
+ // The first three will always be from prepopulated data.
+ std::vector<const TemplateURL*> templateUrls =
+ searchEnginesModel_->GetTemplateURLs();
+
+ // If we have fewer than two search engines, end the search engine dialog
+ // immediately, leaving the imported default search engine setting intact.
+ if (templateUrls.size() < 2) {
+ return;
+ }
+
+ NSWindow* win = [self window];
+
+ [win setBackgroundColor:[NSColor whiteColor]];
+
+ NSImage* headerImage = ResourceBundle::GetSharedInstance().
+ GetNativeImageNamed(IDR_SEARCH_ENGINE_DIALOG_TOP);
+ [headerImageView_ setImage:headerImage];
+
+ // Is the user's default search engine included in the first three
+ // prepopulated set? If not, we need to expand the dialog to include a fourth
+ // engine.
+ const TemplateURL* defaultSearchEngine =
+ searchEnginesModel_->GetDefaultSearchProvider();
+
+ std::vector<const TemplateURL*>::iterator engineIter =
+ templateUrls.begin();
+ for (int i = 0; engineIter != templateUrls.end(); ++i, ++engineIter) {
+ if (i < 3) {
+ choices_.push_back(*engineIter);
+ } else {
+ if (*engineIter == defaultSearchEngine)
+ choices_.push_back(*engineIter);
+ }
+ }
+
+ // Randomize the order of the logos if the option has been set.
+ if (randomize_) {
+ int seed = static_cast<int>(base::Time::Now().ToInternalValue());
+ srand(seed);
+ std::random_shuffle(choices_.begin(), choices_.end());
+ }
+
+ [self buildSearchEngineView];
+
+ // Display the dialog.
+ NSInteger choice = [NSApp runModalForWindow:win];
+ searchEnginesModel_->SetDefaultSearchProvider(choices_.at(choice));
+}
+
+- (void)buildSearchEngineView {
+ scoped_nsobject<NSMutableArray> searchEngineViews
+ ([[NSMutableArray alloc] init]);
+
+ for (size_t i = 0; i < choices_.size(); ++i)
+ [searchEngineViews addObject:[self viewForSearchEngine:choices_.at(i)
+ atIndex:i]];
+
+ NSSize newOverallSize = NSZeroSize;
+ for (NSView* view in searchEngineViews.get()) {
+ NSRect engineFrame = [view frame];
+ engineFrame.origin = NSMakePoint(newOverallSize.width, 0);
+ [searchEngineView_ addSubview:view];
+ [view setFrame:engineFrame];
+ newOverallSize = NSMakeSize(
+ newOverallSize.width + NSWidth(engineFrame) + kSearchEngineSpacing,
+ std::max(newOverallSize.height, NSHeight(engineFrame)));
+ }
+ newOverallSize.width -= kSearchEngineSpacing;
+
+ // Resize the window to fit (and because it's bound on all sides it will
+ // resize the search engine view).
+ NSSize currentOverallSize = [searchEngineView_ bounds].size;
+ NSSize deltaSize = NSMakeSize(
+ newOverallSize.width - currentOverallSize.width,
+ newOverallSize.height - currentOverallSize.height);
+ NSSize windowDeltaSize = [searchEngineView_ convertSize:deltaSize toView:nil];
+ NSRect windowFrame = [[self window] frame];
+ windowFrame.size.width += windowDeltaSize.width;
+ windowFrame.size.height += windowDeltaSize.height;
+ [[self window] setFrame:windowFrame display:NO];
+}
+
+- (NSView*)viewForSearchEngine:(const TemplateURL*)engine
+ atIndex:(size_t)index {
+ bool useImages = false;
+#if defined(GOOGLE_CHROME_BUILD)
+ useImages = true;
+#endif
+
+ // Make the engine identifier.
+ NSView* engineIdentifier = nil; // either the logo or the text label
+
+ int logoId = engine->logo_id();
+ if (useImages && logoId > 0) {
+ NSImage* logoImage =
+ ResourceBundle::GetSharedInstance().GetNativeImageNamed(logoId);
+ NSRect logoBounds = NSZeroRect;
+ logoBounds.size = [logoImage size];
+ NSImageView* logoView =
+ [[[NSImageView alloc] initWithFrame:logoBounds] autorelease];
+ [logoView setImage:logoImage];
+ [logoView setEditable:NO];
+
+ // Tooltip text provides accessibility.
+ [logoView setToolTip:base::SysWideToNSString(engine->short_name())];
+ engineIdentifier = logoView;
+ } else {
+ // No logo -- we must show a text label.
+ NSRect labelBounds = NSMakeRect(0, 0, kLogoLabelWidth, kLogoLabelHeight);
+ NSTextField* labelField =
+ [[[NSTextField alloc] initWithFrame:labelBounds] autorelease];
+ [labelField setBezeled:NO];
+ [labelField setEditable:NO];
+ [labelField setSelectable:NO];
+
+ scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
+ [[NSMutableParagraphStyle alloc] init]);
+ [paragraphStyle setAlignment:NSCenterTextAlignment];
+ NSDictionary* attrs = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSFont boldSystemFontOfSize:13], NSFontAttributeName,
+ paragraphStyle.get(), NSParagraphStyleAttributeName,
+ nil];
+
+ NSString* value = base::SysWideToNSString(engine->short_name());
+ scoped_nsobject<NSAttributedString> attrValue(
+ [[NSAttributedString alloc] initWithString:value
+ attributes:attrs]);
+
+ [labelField setAttributedStringValue:attrValue.get()];
+
+ engineIdentifier = labelField;
+ }
+
+ // Make the "Choose" button.
+ scoped_nsobject<NSButton> chooseButton(
+ [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 100, 34)]);
+ [chooseButton setBezelStyle:NSRoundedBezelStyle];
+ [[chooseButton cell] setFont:[NSFont systemFontOfSize:
+ [NSFont systemFontSizeForControlSize:NSRegularControlSize]]];
+ [chooseButton setTitle:l10n_util::GetNSStringWithFixup(IDS_FR_SEARCH_CHOOSE)];
+ [GTMUILocalizerAndLayoutTweaker sizeToFitView:chooseButton.get()];
+ [chooseButton setTag:index];
+ [chooseButton setTarget:self];
+ [chooseButton setAction:@selector(searchEngineSelected:)];
+
+ // Put 'em together.
+ NSRect engineIdentifierFrame = [engineIdentifier frame];
+ NSRect chooseButtonFrame = [chooseButton frame];
+
+ NSRect containingViewFrame = NSZeroRect;
+ containingViewFrame.size.width += engineIdentifierFrame.size.width;
+ containingViewFrame.size.height += engineIdentifierFrame.size.height;
+ containingViewFrame.size.height += kLogoButtonSpacing;
+ containingViewFrame.size.height += chooseButtonFrame.size.height;
+
+ NSView* containingView =
+ [[[NSView alloc] initWithFrame:containingViewFrame] autorelease];
+
+ [containingView addSubview:engineIdentifier];
+ engineIdentifierFrame.origin.y =
+ chooseButtonFrame.size.height + kLogoButtonSpacing;
+ [engineIdentifier setFrame:engineIdentifierFrame];
+
+ [containingView addSubview:chooseButton];
+ chooseButtonFrame.origin.x =
+ int((containingViewFrame.size.width - chooseButtonFrame.size.width) / 2);
+ [chooseButton setFrame:chooseButtonFrame];
+
+ return containingView;
+}
+
+- (NSFont*)mainLabelFont {
+ return [NSFont boldSystemFontOfSize:13];
+}
+
+- (IBAction)searchEngineSelected:(id)sender {
+ [[self window] close];
+ [NSApp stopModalWithCode:[sender tag]];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/search_engine_list_model.h b/chrome/browser/ui/cocoa/search_engine_list_model.h
new file mode 100644
index 0000000..f42fe35
--- /dev/null
+++ b/chrome/browser/ui/cocoa/search_engine_list_model.h
@@ -0,0 +1,48 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_SEARCH_ENGINE_LIST_MODEL_H_
+#define CHROME_BROWSER_UI_COCOA_SEARCH_ENGINE_LIST_MODEL_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+
+class TemplateURLModel;
+class SearchEngineObserver;
+
+// The model for the "default search engine" combobox in preferences. Bridges
+// between the cross-platform TemplateURLModel and Cocoa while watching for
+// changes to the cross-platform model.
+
+@interface SearchEngineListModel : NSObject {
+ @private
+ TemplateURLModel* model_; // weak, owned by Profile
+ scoped_ptr<SearchEngineObserver> observer_; // watches for model changes
+ scoped_nsobject<NSArray> engines_;
+}
+
+// Initialize with the given template model.
+- (id)initWithModel:(TemplateURLModel*)model;
+
+// Returns an array of NSString's corresponding to the user-visible names of the
+// search engines.
+- (NSArray*)searchEngines;
+
+// The index into |-searchEngines| of the current default search engine. If
+// there is no default search engine, the value is -1. The setter changes the
+// back-end preference.
+- (NSInteger)defaultIndex;
+- (void)setDefaultIndex:(NSInteger)index;
+// Return TRUE if the default is managed via policy.
+- (BOOL)isDefaultManaged;
+@end
+
+// Broadcast when the cross-platform model changes. This can be used to update
+// any view state that may rely on the position of items in the list.
+extern NSString* const kSearchEngineListModelChangedNotification;
+
+#endif // CHROME_BROWSER_UI_COCOA_SEARCH_ENGINE_LIST_MODEL_H_
diff --git a/chrome/browser/ui/cocoa/search_engine_list_model.mm b/chrome/browser/ui/cocoa/search_engine_list_model.mm
new file mode 100644
index 0000000..b79c882
--- /dev/null
+++ b/chrome/browser/ui/cocoa/search_engine_list_model.mm
@@ -0,0 +1,136 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/search_engine_list_model.h"
+
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/search_engines/template_url.h"
+#include "chrome/browser/search_engines/template_url_model.h"
+#include "chrome/browser/search_engines/template_url_model_observer.h"
+
+NSString* const kSearchEngineListModelChangedNotification =
+ @"kSearchEngineListModelChangedNotification";
+
+@interface SearchEngineListModel(Private)
+- (void)buildEngineList;
+@end
+
+// C++ bridge from TemplateURLModel to our Obj-C model. When it's told about
+// model changes, notifies us to rebuild the list.
+class SearchEngineObserver : public TemplateURLModelObserver {
+ public:
+ SearchEngineObserver(SearchEngineListModel* notify)
+ : notify_(notify) { }
+ virtual ~SearchEngineObserver() { };
+
+ private:
+ // TemplateURLModelObserver methods.
+ virtual void OnTemplateURLModelChanged() { [notify_ buildEngineList]; }
+
+ SearchEngineListModel* notify_; // weak, owns us
+};
+
+@implementation SearchEngineListModel
+
+// The windows code allows for a NULL |model| and checks for it throughout
+// the code, though I'm not sure why. We follow suit.
+- (id)initWithModel:(TemplateURLModel*)model {
+ if ((self = [super init])) {
+ model_ = model;
+ if (model_) {
+ observer_.reset(new SearchEngineObserver(self));
+ model_->Load();
+ model_->AddObserver(observer_.get());
+ [self buildEngineList];
+ }
+ }
+ return self;
+}
+
+- (void)dealloc {
+ if (model_)
+ model_->RemoveObserver(observer_.get());
+ [super dealloc];
+}
+
+// Returns an array of NSString's corresponding to the user-visible names of the
+// search engines.
+- (NSArray*)searchEngines {
+ return engines_.get();
+}
+
+- (void)setSearchEngines:(NSArray*)engines {
+ engines_.reset([engines retain]);
+
+ // Tell anyone who's listening that something has changed so they need to
+ // adjust the UI.
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kSearchEngineListModelChangedNotification
+ object:nil];
+}
+
+// Walks the model and builds an array of NSStrings to display to the user.
+// Assumes there is a non-NULL model.
+- (void)buildEngineList {
+ scoped_nsobject<NSMutableArray> engines([[NSMutableArray alloc] init]);
+
+ typedef std::vector<const TemplateURL*> TemplateURLs;
+ TemplateURLs modelURLs = model_->GetTemplateURLs();
+ for (size_t i = 0; i < modelURLs.size(); ++i) {
+ if (modelURLs[i]->ShowInDefaultList())
+ [engines addObject:base::SysWideToNSString(modelURLs[i]->short_name())];
+ }
+
+ [self setSearchEngines:engines.get()];
+}
+
+// The index into |-searchEngines| of the current default search engine.
+// -1 if there is no default.
+- (NSInteger)defaultIndex {
+ if (!model_) return -1;
+
+ NSInteger index = 0;
+ const TemplateURL* defaultSearchProvider = model_->GetDefaultSearchProvider();
+ if (defaultSearchProvider) {
+ typedef std::vector<const TemplateURL*> TemplateURLs;
+ TemplateURLs urls = model_->GetTemplateURLs();
+ for (std::vector<const TemplateURL*>::iterator it = urls.begin();
+ it != urls.end(); ++it) {
+ const TemplateURL* url = *it;
+ // Skip all the URLs not shown on the default list.
+ if (!url->ShowInDefaultList())
+ continue;
+ if (url->id() == defaultSearchProvider->id())
+ return index;
+ ++index;
+ }
+ }
+ return -1;
+}
+
+- (void)setDefaultIndex:(NSInteger)index {
+ if (model_) {
+ typedef std::vector<const TemplateURL*> TemplateURLs;
+ TemplateURLs urls = model_->GetTemplateURLs();
+ for (std::vector<const TemplateURL*>::iterator it = urls.begin();
+ it != urls.end(); ++it) {
+ const TemplateURL* url = *it;
+ // Skip all the URLs not shown on the default list.
+ if (!url->ShowInDefaultList())
+ continue;
+ if (0 == index) {
+ model_->SetDefaultSearchProvider(url);
+ return;
+ }
+ --index;
+ }
+ DCHECK(false);
+ }
+}
+
+// Return TRUE if the default is managed via policy.
+- (BOOL)isDefaultManaged {
+ return model_->is_default_search_managed();
+}
+@end
diff --git a/chrome/browser/ui/cocoa/search_engine_list_model_unittest.mm b/chrome/browser/ui/cocoa/search_engine_list_model_unittest.mm
new file mode 100644
index 0000000..c95c75c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/search_engine_list_model_unittest.mm
@@ -0,0 +1,152 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/search_engines/template_url.h"
+#include "chrome/browser/search_engines/template_url_model.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/search_engine_list_model.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+// A helper for NSNotifications. Makes a note that it's been called back.
+@interface SearchEngineListHelper : NSObject {
+ @public
+ BOOL sawNotification_;
+}
+@end
+
+@implementation SearchEngineListHelper
+- (void)entryChanged:(NSNotification*)notify {
+ sawNotification_ = YES;
+}
+@end
+
+class SearchEngineListModelTest : public PlatformTest {
+ public:
+ SearchEngineListModelTest() {
+ // Build a fake set of template urls.
+ template_model_.reset(new TemplateURLModel(helper_.profile()));
+ TemplateURL* t_url = new TemplateURL();
+ t_url->SetURL("http://www.google.com/?q={searchTerms}", 0, 0);
+ t_url->set_keyword(L"keyword");
+ t_url->set_short_name(L"google");
+ t_url->set_show_in_default_list(true);
+ template_model_->Add(t_url);
+ t_url = new TemplateURL();
+ t_url->SetURL("http://www.google2.com/?q={searchTerms}", 0, 0);
+ t_url->set_keyword(L"keyword2");
+ t_url->set_short_name(L"google2");
+ t_url->set_show_in_default_list(true);
+ template_model_->Add(t_url);
+ EXPECT_EQ(template_model_->GetTemplateURLs().size(), 2U);
+
+ model_.reset([[SearchEngineListModel alloc]
+ initWithModel:template_model_.get()]);
+ notification_helper_.reset([[SearchEngineListHelper alloc] init]);
+ [[NSNotificationCenter defaultCenter]
+ addObserver:notification_helper_.get()
+ selector:@selector(entryChanged:)
+ name:kSearchEngineListModelChangedNotification
+ object:nil];
+ }
+ ~SearchEngineListModelTest() {
+ [[NSNotificationCenter defaultCenter]
+ removeObserver:notification_helper_.get()];
+ }
+
+ BrowserTestHelper helper_;
+ scoped_ptr<TemplateURLModel> template_model_;
+ scoped_nsobject<SearchEngineListModel> model_;
+ scoped_nsobject<SearchEngineListHelper> notification_helper_;
+};
+
+TEST_F(SearchEngineListModelTest, Init) {
+ scoped_nsobject<SearchEngineListModel> model(
+ [[SearchEngineListModel alloc] initWithModel:template_model_.get()]);
+}
+
+TEST_F(SearchEngineListModelTest, Engines) {
+ NSArray* engines = [model_ searchEngines];
+ EXPECT_EQ([engines count], 2U);
+}
+
+TEST_F(SearchEngineListModelTest, Default) {
+ EXPECT_EQ([model_ defaultIndex], -1);
+
+ [model_ setDefaultIndex:1];
+ EXPECT_EQ([model_ defaultIndex], 1);
+
+ // Add two more URLs, neither of which are shown in the default list.
+ TemplateURL* t_url = new TemplateURL();
+ t_url->SetURL("http://www.google3.com/?q={searchTerms}", 0, 0);
+ t_url->set_keyword(L"keyword3");
+ t_url->set_short_name(L"google3 not eligible");
+ t_url->set_show_in_default_list(false);
+ template_model_->Add(t_url);
+ t_url = new TemplateURL();
+ t_url->SetURL("http://www.google4.com/?q={searchTerms}", 0, 0);
+ t_url->set_keyword(L"keyword4");
+ t_url->set_short_name(L"google4");
+ t_url->set_show_in_default_list(false);
+ template_model_->Add(t_url);
+
+ // Still should only have 2 engines and not these newly added ones.
+ EXPECT_EQ([[model_ searchEngines] count], 2U);
+
+ // Since keyword3 is not in the default list, the 2nd index in the default
+ // keyword list should be keyword4. Test for http://crbug.com/21898.
+ template_model_->SetDefaultSearchProvider(t_url);
+ EXPECT_EQ([[model_ searchEngines] count], 3U);
+ EXPECT_EQ([model_ defaultIndex], 2);
+
+ NSString* defaultString = [[model_ searchEngines] objectAtIndex:2];
+ EXPECT_NSEQ(@"google4", defaultString);
+}
+
+TEST_F(SearchEngineListModelTest, DefaultChosenFromUI) {
+ EXPECT_EQ([model_ defaultIndex], -1);
+
+ [model_ setDefaultIndex:1];
+ EXPECT_EQ([model_ defaultIndex], 1);
+
+ // Add two more URLs, the first one not shown in the default list.
+ TemplateURL* t_url = new TemplateURL();
+ t_url->SetURL("http://www.google3.com/?q={searchTerms}", 0, 0);
+ t_url->set_keyword(L"keyword3");
+ t_url->set_short_name(L"google3 not eligible");
+ t_url->set_show_in_default_list(false);
+ template_model_->Add(t_url);
+ t_url = new TemplateURL();
+ t_url->SetURL("http://www.google4.com/?q={searchTerms}", 0, 0);
+ t_url->set_keyword(L"keyword4");
+ t_url->set_short_name(L"google4");
+ t_url->set_show_in_default_list(true);
+ template_model_->Add(t_url);
+
+ // We should have 3 engines.
+ EXPECT_EQ([[model_ searchEngines] count], 3U);
+
+ // Simulate the UI setting the default to the third entry.
+ [model_ setDefaultIndex:2];
+ EXPECT_EQ([model_ defaultIndex], 2);
+
+ // The default search provider should be google4.
+ EXPECT_EQ(template_model_->GetDefaultSearchProvider(), t_url);
+}
+
+// Make sure that when the back-end model changes that we get a notification.
+TEST_F(SearchEngineListModelTest, Notification) {
+ // Add one more item to force a notification.
+ TemplateURL* t_url = new TemplateURL();
+ t_url->SetURL("http://www.google3.com/foo/bar", 0, 0);
+ t_url->set_keyword(L"keyword3");
+ t_url->set_short_name(L"google3");
+ t_url->set_show_in_default_list(true);
+ template_model_->Add(t_url);
+
+ EXPECT_TRUE(notification_helper_.get()->sawNotification_);
+}
diff --git a/chrome/browser/ui/cocoa/shell_dialogs_mac.mm b/chrome/browser/ui/cocoa/shell_dialogs_mac.mm
new file mode 100644
index 0000000..8dcaebed6
--- /dev/null
+++ b/chrome/browser/ui/cocoa/shell_dialogs_mac.mm
@@ -0,0 +1,417 @@
+// Copyright (c) 2006-2008 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 "chrome/browser/shell_dialogs.h"
+
+#import <Cocoa/Cocoa.h>
+#include <CoreServices/CoreServices.h>
+
+#include <map>
+#include <set>
+#include <vector>
+
+#include "app/l10n_util_mac.h"
+#import "base/cocoa_protocols_mac.h"
+#include "base/file_util.h"
+#include "base/logging.h"
+#include "base/mac_util.h"
+#include "base/mac/scoped_cftyperef.h"
+#import "base/scoped_nsobject.h"
+#include "base/sys_string_conversions.h"
+#include "base/thread_restrictions.h"
+#include "grit/generated_resources.h"
+
+static const int kFileTypePopupTag = 1234;
+
+class SelectFileDialogImpl;
+
+// A bridge class to act as the modal delegate to the save/open sheet and send
+// the results to the C++ class.
+@interface SelectFileDialogBridge : NSObject<NSOpenSavePanelDelegate> {
+ @private
+ SelectFileDialogImpl* selectFileDialogImpl_; // WEAK; owns us
+}
+
+- (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s;
+- (void)endedPanel:(NSSavePanel*)panel
+ withReturn:(int)returnCode
+ context:(void *)context;
+
+// NSSavePanel delegate method
+- (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename;
+
+@end
+
+// Implementation of SelectFileDialog that shows Cocoa dialogs for choosing a
+// file or folder.
+class SelectFileDialogImpl : public SelectFileDialog {
+ public:
+ explicit SelectFileDialogImpl(Listener* listener);
+ virtual ~SelectFileDialogImpl();
+
+ // BaseShellDialog implementation.
+ virtual bool IsRunning(gfx::NativeWindow parent_window) const;
+ virtual void ListenerDestroyed();
+
+ // SelectFileDialog implementation.
+ // |params| is user data we pass back via the Listener interface.
+ virtual void SelectFile(Type type,
+ const string16& title,
+ const FilePath& default_path,
+ const FileTypeInfo* file_types,
+ int file_type_index,
+ const FilePath::StringType& default_extension,
+ gfx::NativeWindow owning_window,
+ void* params);
+
+ // Callback from ObjC bridge.
+ void FileWasSelected(NSSavePanel* dialog,
+ NSWindow* parent_window,
+ bool was_cancelled,
+ bool is_multi,
+ const std::vector<FilePath>& files,
+ int index);
+
+ bool ShouldEnableFilename(NSSavePanel* dialog, NSString* filename);
+
+ struct SheetContext {
+ Type type;
+ NSWindow* owning_window;
+ };
+
+ private:
+ // Gets the accessory view for the save dialog.
+ NSView* GetAccessoryView(const FileTypeInfo* file_types,
+ int file_type_index);
+
+ // The listener to be notified of selection completion.
+ Listener* listener_;
+
+ // The bridge for results from Cocoa to return to us.
+ scoped_nsobject<SelectFileDialogBridge> bridge_;
+
+ // A map from file dialogs to the |params| user data associated with them.
+ std::map<NSSavePanel*, void*> params_map_;
+
+ // The set of all parent windows for which we are currently running dialogs.
+ std::set<NSWindow*> parents_;
+
+ // A map from file dialogs to their types.
+ std::map<NSSavePanel*, Type> type_map_;
+
+ DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImpl);
+};
+
+// static
+SelectFileDialog* SelectFileDialog::Create(Listener* listener) {
+ return new SelectFileDialogImpl(listener);
+}
+
+SelectFileDialogImpl::SelectFileDialogImpl(Listener* listener)
+ : listener_(listener),
+ bridge_([[SelectFileDialogBridge alloc]
+ initWithSelectFileDialogImpl:this]) {
+}
+
+SelectFileDialogImpl::~SelectFileDialogImpl() {
+ // Walk through the open dialogs and close them all. Use a temporary vector
+ // to hold the pointers, since we can't delete from the map as we're iterating
+ // through it.
+ std::vector<NSSavePanel*> panels;
+ for (std::map<NSSavePanel*, void*>::iterator it = params_map_.begin();
+ it != params_map_.end(); ++it) {
+ panels.push_back(it->first);
+ }
+
+ for (std::vector<NSSavePanel*>::iterator it = panels.begin();
+ it != panels.end(); ++it) {
+ [(*it) cancel:nil];
+ }
+}
+
+bool SelectFileDialogImpl::IsRunning(gfx::NativeWindow parent_window) const {
+ return parents_.find(parent_window) != parents_.end();
+}
+
+void SelectFileDialogImpl::ListenerDestroyed() {
+ listener_ = NULL;
+}
+
+void SelectFileDialogImpl::SelectFile(
+ Type type,
+ const string16& title,
+ const FilePath& default_path,
+ const FileTypeInfo* file_types,
+ int file_type_index,
+ const FilePath::StringType& default_extension,
+ gfx::NativeWindow owning_window,
+ void* params) {
+ DCHECK(type == SELECT_FOLDER ||
+ type == SELECT_OPEN_FILE ||
+ type == SELECT_OPEN_MULTI_FILE ||
+ type == SELECT_SAVEAS_FILE);
+ parents_.insert(owning_window);
+
+ // Note: we need to retain the dialog as owning_window can be null.
+ // (see http://crbug.com/29213)
+ NSSavePanel* dialog;
+ if (type == SELECT_SAVEAS_FILE)
+ dialog = [[NSSavePanel savePanel] retain];
+ else
+ dialog = [[NSOpenPanel openPanel] retain];
+
+ if (!title.empty())
+ [dialog setTitle:base::SysUTF16ToNSString(title)];
+
+ NSString* default_dir = nil;
+ NSString* default_filename = nil;
+ if (!default_path.empty()) {
+ // The file dialog is going to do a ton of stats anyway. Not much
+ // point in eliminating this one.
+ base::ThreadRestrictions::ScopedAllowIO allow_io;
+ if (file_util::DirectoryExists(default_path)) {
+ default_dir = base::SysUTF8ToNSString(default_path.value());
+ } else {
+ default_dir = base::SysUTF8ToNSString(default_path.DirName().value());
+ default_filename =
+ base::SysUTF8ToNSString(default_path.BaseName().value());
+ }
+ }
+
+ NSMutableArray* allowed_file_types = nil;
+ if (file_types) {
+ if (!file_types->extensions.empty()) {
+ allowed_file_types = [NSMutableArray array];
+ for (size_t i=0; i < file_types->extensions.size(); ++i) {
+ const std::vector<FilePath::StringType>& ext_list =
+ file_types->extensions[i];
+ for (size_t j=0; j < ext_list.size(); ++j) {
+ [allowed_file_types addObject:base::SysUTF8ToNSString(ext_list[j])];
+ }
+ }
+ }
+ if (type == SELECT_SAVEAS_FILE)
+ [dialog setAllowedFileTypes:allowed_file_types];
+ // else we'll pass it in when we run the open panel
+
+ if (file_types->include_all_files)
+ [dialog setAllowsOtherFileTypes:YES];
+
+ if (!file_types->extension_description_overrides.empty()) {
+ NSView* accessory_view = GetAccessoryView(file_types, file_type_index);
+ [dialog setAccessoryView:accessory_view];
+ }
+ } else {
+ // If no type info is specified, anything goes.
+ [dialog setAllowsOtherFileTypes:YES];
+ }
+
+ if (!default_extension.empty())
+ [dialog setRequiredFileType:base::SysUTF8ToNSString(default_extension)];
+
+ params_map_[dialog] = params;
+ type_map_[dialog] = type;
+
+ SheetContext* context = new SheetContext;
+
+ // |context| should never be NULL, but we are seeing indications otherwise.
+ // |This CHECK is here to confirm if we are actually getting NULL
+ // ||context|s. http://crbug.com/58959
+ CHECK(context);
+ context->type = type;
+ context->owning_window = owning_window;
+
+ if (type == SELECT_SAVEAS_FILE) {
+ [dialog beginSheetForDirectory:default_dir
+ file:default_filename
+ modalForWindow:owning_window
+ modalDelegate:bridge_.get()
+ didEndSelector:@selector(endedPanel:withReturn:context:)
+ contextInfo:context];
+ } else {
+ NSOpenPanel* open_dialog = (NSOpenPanel*)dialog;
+
+ if (type == SELECT_OPEN_MULTI_FILE)
+ [open_dialog setAllowsMultipleSelection:YES];
+ else
+ [open_dialog setAllowsMultipleSelection:NO];
+
+ if (type == SELECT_FOLDER) {
+ [open_dialog setCanChooseFiles:NO];
+ [open_dialog setCanChooseDirectories:YES];
+ [open_dialog setCanCreateDirectories:YES];
+ NSString *prompt = l10n_util::GetNSString(IDS_SELECT_FOLDER_BUTTON_TITLE);
+ [open_dialog setPrompt:prompt];
+ } else {
+ [open_dialog setCanChooseFiles:YES];
+ [open_dialog setCanChooseDirectories:NO];
+ }
+
+ [open_dialog setDelegate:bridge_.get()];
+ [open_dialog beginSheetForDirectory:default_dir
+ file:default_filename
+ types:allowed_file_types
+ modalForWindow:owning_window
+ modalDelegate:bridge_.get()
+ didEndSelector:@selector(endedPanel:withReturn:context:)
+ contextInfo:context];
+ }
+}
+
+void SelectFileDialogImpl::FileWasSelected(NSSavePanel* dialog,
+ NSWindow* parent_window,
+ bool was_cancelled,
+ bool is_multi,
+ const std::vector<FilePath>& files,
+ int index) {
+ void* params = params_map_[dialog];
+ params_map_.erase(dialog);
+ parents_.erase(parent_window);
+ type_map_.erase(dialog);
+
+ if (!listener_)
+ return;
+
+ if (was_cancelled) {
+ listener_->FileSelectionCanceled(params);
+ } else {
+ if (is_multi) {
+ listener_->MultiFilesSelected(files, params);
+ } else {
+ listener_->FileSelected(files[0], index, params);
+ }
+ }
+}
+
+NSView* SelectFileDialogImpl::GetAccessoryView(const FileTypeInfo* file_types,
+ int file_type_index) {
+ DCHECK(file_types);
+ scoped_nsobject<NSNib> nib (
+ [[NSNib alloc] initWithNibNamed:@"SaveAccessoryView"
+ bundle:mac_util::MainAppBundle()]);
+ if (!nib)
+ return nil;
+
+ NSArray* objects;
+ BOOL success = [nib instantiateNibWithOwner:nil
+ topLevelObjects:&objects];
+ if (!success)
+ return nil;
+ [objects makeObjectsPerformSelector:@selector(release)];
+
+ // This is a one-object nib, but IB insists on creating a second object, the
+ // NSApplication. I don't know why.
+ size_t view_index = 0;
+ while (view_index < [objects count] &&
+ ![[objects objectAtIndex:view_index] isKindOfClass:[NSView class]])
+ ++view_index;
+ DCHECK(view_index < [objects count]);
+ NSView* accessory_view = [objects objectAtIndex:view_index];
+
+ NSPopUpButton* popup = [accessory_view viewWithTag:kFileTypePopupTag];
+ DCHECK(popup);
+
+ size_t type_count = file_types->extensions.size();
+ for (size_t type = 0; type<type_count; ++type) {
+ NSString* type_description;
+ if (type < file_types->extension_description_overrides.size()) {
+ type_description = base::SysUTF16ToNSString(
+ file_types->extension_description_overrides[type]);
+ } else {
+ const std::vector<FilePath::StringType>& ext_list =
+ file_types->extensions[type];
+ DCHECK(!ext_list.empty());
+ NSString* type_extension = base::SysUTF8ToNSString(ext_list[0]);
+ base::mac::ScopedCFTypeRef<CFStringRef> uti(
+ UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension,
+ (CFStringRef)type_extension,
+ NULL));
+ base::mac::ScopedCFTypeRef<CFStringRef> description(
+ UTTypeCopyDescription(uti.get()));
+
+ type_description =
+ [NSString stringWithString:(NSString*)description.get()];
+ }
+ [popup addItemWithTitle:type_description];
+ }
+
+ [popup selectItemAtIndex:file_type_index-1]; // 1-based
+ return accessory_view;
+}
+
+bool SelectFileDialogImpl::ShouldEnableFilename(NSSavePanel* dialog,
+ NSString* filename) {
+ // If this is a single open file dialog, disable selecting packages.
+ if (type_map_[dialog] != SELECT_OPEN_FILE)
+ return true;
+
+ return ![[NSWorkspace sharedWorkspace] isFilePackageAtPath:filename];
+}
+
+@implementation SelectFileDialogBridge
+
+- (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s {
+ self = [super init];
+ if (self != nil) {
+ selectFileDialogImpl_ = s;
+ }
+ return self;
+}
+
+- (void)endedPanel:(NSSavePanel*)panel
+ withReturn:(int)returnCode
+ context:(void *)context {
+ // |context| should never be NULL, but we are seeing indications otherwise.
+ // |This CHECK is here to confirm if we are actually getting NULL
+ // ||context|s. http://crbug.com/58959
+ CHECK(context);
+
+ int index = 0;
+ SelectFileDialogImpl::SheetContext* context_struct =
+ (SelectFileDialogImpl::SheetContext*)context;
+
+ SelectFileDialog::Type type = context_struct->type;
+ NSWindow* parentWindow = context_struct->owning_window;
+ delete context_struct;
+
+ bool isMulti = type == SelectFileDialog::SELECT_OPEN_MULTI_FILE;
+
+ std::vector<FilePath> paths;
+ bool did_cancel = returnCode == NSCancelButton;
+ if (!did_cancel) {
+ if (type == SelectFileDialog::SELECT_SAVEAS_FILE) {
+ paths.push_back(FilePath(base::SysNSStringToUTF8([panel filename])));
+
+ NSView* accessoryView = [panel accessoryView];
+ if (accessoryView) {
+ NSPopUpButton* popup = [accessoryView viewWithTag:kFileTypePopupTag];
+ if (popup) {
+ // File type indexes are 1-based.
+ index = [popup indexOfSelectedItem] + 1;
+ }
+ } else {
+ index = 1;
+ }
+ } else {
+ CHECK([panel isKindOfClass:[NSOpenPanel class]]);
+ NSArray* filenames = [static_cast<NSOpenPanel*>(panel) filenames];
+ for (NSString* filename in filenames)
+ paths.push_back(FilePath(base::SysNSStringToUTF8(filename)));
+ }
+ }
+
+ selectFileDialogImpl_->FileWasSelected(panel,
+ parentWindow,
+ did_cancel,
+ isMulti,
+ paths,
+ index);
+ [panel release];
+}
+
+- (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename {
+ return selectFileDialogImpl_->ShouldEnableFilename(sender, filename);
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/side_tab_strip_controller.h b/chrome/browser/ui/cocoa/side_tab_strip_controller.h
new file mode 100644
index 0000000..07f5551
--- /dev/null
+++ b/chrome/browser/ui/cocoa/side_tab_strip_controller.h
@@ -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 <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/tab_strip_controller.h"
+
+// A controller for the tab strip when side tabs are enabled.
+//
+// TODO(pinkerton): I'm expecting there are more things here that need
+// overriding rather than just tweaking a couple of settings, so I'm creating
+// a full-blown subclass. Clearly, very little is actually necessary at this
+// point for it to work.
+
+@interface SideTabStripController : TabStripController {
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/side_tab_strip_controller.mm b/chrome/browser/ui/cocoa/side_tab_strip_controller.mm
new file mode 100644
index 0000000..16a835f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/side_tab_strip_controller.mm
@@ -0,0 +1,33 @@
+// 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/ui/cocoa/side_tab_strip_controller.h"
+
+@implementation SideTabStripController
+
+// TODO(pinkerton): Still need to figure out several things:
+// - new tab button placement and layout
+// - animating tabs in and out
+// - being able to drop a tab elsewhere besides the 1st position
+// - how to load a different tab view nib for each tab.
+
+- (id)initWithView:(TabStripView*)view
+ switchView:(NSView*)switchView
+ browser:(Browser*)browser
+ delegate:(id<TabStripControllerDelegate>)delegate {
+ self = [super initWithView:view
+ switchView:switchView
+ browser:browser
+ delegate:delegate];
+ if (self) {
+ // Side tabs have no indent since they are not sharing space with the
+ // window controls.
+ [self setIndentForControls:0.0];
+ verticalLayout_ = YES;
+ }
+ return self;
+}
+
+@end
+
diff --git a/chrome/browser/ui/cocoa/side_tab_strip_view.h b/chrome/browser/ui/cocoa/side_tab_strip_view.h
new file mode 100644
index 0000000..9f8d056
--- /dev/null
+++ b/chrome/browser/ui/cocoa/side_tab_strip_view.h
@@ -0,0 +1,15 @@
+// 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/ui/cocoa/tab_strip_view.h"
+
+// A class that handles drawing the background of the tab strip when side tabs
+// are enabled.
+
+@interface SideTabStripView : TabStripView {
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/side_tab_strip_view.mm b/chrome/browser/ui/cocoa/side_tab_strip_view.mm
new file mode 100644
index 0000000..2c60604
--- /dev/null
+++ b/chrome/browser/ui/cocoa/side_tab_strip_view.mm
@@ -0,0 +1,43 @@
+// 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/ui/cocoa/side_tab_strip_view.h"
+
+#include "base/scoped_nsobject.h"
+#import "third_party/GTM/AppKit/GTMNSColor+Luminance.h"
+
+@implementation SideTabStripView
+
+- (void)drawBorder:(NSRect)bounds {
+ // Draw a border on the right side.
+ NSRect borderRect, contentRect;
+ NSDivideRect(bounds, &borderRect, &contentRect, 1, NSMaxXEdge);
+ [[NSColor colorWithCalibratedWhite:0.0 alpha:0.2] set];
+ NSRectFillUsingOperation(borderRect, NSCompositeSourceOver);
+}
+
+// Override to prevent double-clicks from minimizing the window. The side
+// tab strip doesn't have that behavior (since it's in the window content
+// area).
+- (BOOL)doubleClickMinimizesWindow {
+ return NO;
+}
+
+- (void)drawRect:(NSRect)rect {
+ // BOOL isKey = [[self window] isKeyWindow];
+ NSColor* aColor =
+ [NSColor colorWithCalibratedRed:0.506 green:0.660 blue:0.985 alpha:1.000];
+ NSColor* bColor =
+ [NSColor colorWithCalibratedRed:0.099 green:0.140 blue:0.254 alpha:1.000];
+ scoped_nsobject<NSGradient> gradient(
+ [[NSGradient alloc] initWithStartingColor:aColor endingColor:bColor]);
+
+ NSRect gradientRect = [self bounds];
+ [gradient drawInRect:gradientRect angle:270.0];
+
+ // Draw borders and any drop feedback.
+ [super drawRect:rect];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/side_tab_strip_view_unittest.mm b/chrome/browser/ui/cocoa/side_tab_strip_view_unittest.mm
new file mode 100644
index 0000000..ba7acc2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/side_tab_strip_view_unittest.mm
@@ -0,0 +1,30 @@
+// 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/ui/cocoa/side_tab_strip_view.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class SideTabStripViewTest : public CocoaTest {
+ public:
+ SideTabStripViewTest() {
+ NSRect frame = NSMakeRect(0, 0, 100, 30);
+ scoped_nsobject<SideTabStripView> view(
+ [[SideTabStripView alloc] initWithFrame:frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ SideTabStripView* view_;
+};
+
+TEST_VIEW(SideTabStripViewTest, view_)
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/sidebar_controller.h b/chrome/browser/ui/cocoa/sidebar_controller.h
new file mode 100644
index 0000000..dcafddb
--- /dev/null
+++ b/chrome/browser/ui/cocoa/sidebar_controller.h
@@ -0,0 +1,51 @@
+// 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_UI_COCOA_SIDEBAR_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_SIDEBAR_CONTROLLER_H_
+#pragma once
+
+#import <Foundation/Foundation.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/tab_contents_controller.h"
+
+@class NSSplitView;
+@class NSView;
+
+class TabContents;
+
+// A class that handles updates of the sidebar view within a browser window.
+// It swaps in the relevant sidebar contents for a given TabContents or removes
+// the vew, if there's no sidebar contents to show.
+@interface SidebarController : NSObject {
+ @private
+ // A view hosting sidebar contents.
+ scoped_nsobject<NSSplitView> splitView_;
+
+ // Manages currently displayed sidebar contents.
+ scoped_nsobject<TabContentsController> contentsController_;
+}
+
+- (id)initWithDelegate:(id<TabContentsControllerDelegate>)delegate;
+
+// This controller's view.
+- (NSSplitView*)view;
+
+// The compiler seems to have trouble handling a function named "view" that
+// returns an NSSplitView, so provide a differently-named method.
+- (NSSplitView*)splitView;
+
+// Depending on |contents|'s state, decides whether the sidebar
+// should be shown or hidden and adjusts its width (|delegate_| handles
+// the actual resize).
+- (void)updateSidebarForTabContents:(TabContents*)contents;
+
+// Call when the sidebar view is properly sized and the render widget host view
+// should be put into the view hierarchy.
+- (void)ensureContentsVisible;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_SIDEBAR_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/sidebar_controller.mm b/chrome/browser/ui/cocoa/sidebar_controller.mm
new file mode 100644
index 0000000..f20deaa
--- /dev/null
+++ b/chrome/browser/ui/cocoa/sidebar_controller.mm
@@ -0,0 +1,179 @@
+// 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/ui/cocoa/sidebar_controller.h"
+
+#include <algorithm>
+
+#include <Cocoa/Cocoa.h>
+
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/sidebar/sidebar_manager.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/ui/browser.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+#include "chrome/common/pref_names.h"
+
+namespace {
+
+// By default sidebar width is 1/7th of the current page content width.
+const CGFloat kDefaultSidebarWidthRatio = 1.0 / 7;
+
+// Never make the web part of the tab contents smaller than this (needed if the
+// window is only a few pixels wide).
+const int kMinWebWidth = 50;
+
+} // end namespace
+
+
+@interface SidebarController (Private)
+- (void)showSidebarContents:(TabContents*)sidebarContents;
+- (void)resizeSidebarToNewWidth:(CGFloat)width;
+@end
+
+
+@implementation SidebarController
+
+- (id)initWithDelegate:(id<TabContentsControllerDelegate>)delegate {
+ if ((self = [super init])) {
+ splitView_.reset([[NSSplitView alloc] initWithFrame:NSZeroRect]);
+ [splitView_ setDividerStyle:NSSplitViewDividerStyleThin];
+ [splitView_ setVertical:YES];
+ [splitView_ setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
+ [splitView_ setDelegate:self];
+
+ contentsController_.reset(
+ [[TabContentsController alloc] initWithContents:NULL
+ delegate:delegate]);
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [splitView_ setDelegate:nil];
+ [super dealloc];
+}
+
+- (NSSplitView*)view {
+ return splitView_.get();
+}
+
+- (NSSplitView*)splitView {
+ return splitView_.get();
+}
+
+- (void)updateSidebarForTabContents:(TabContents*)contents {
+ // Get the active sidebar content.
+ if (SidebarManager::GetInstance() == NULL) // Happens in tests.
+ return;
+
+ TabContents* sidebarContents = NULL;
+ if (contents && SidebarManager::IsSidebarAllowed()) {
+ SidebarContainer* activeSidebar =
+ SidebarManager::GetInstance()->GetActiveSidebarContainerFor(contents);
+ if (activeSidebar)
+ sidebarContents = activeSidebar->sidebar_contents();
+ }
+
+ TabContents* oldSidebarContents = [contentsController_ tabContents];
+ if (oldSidebarContents == sidebarContents)
+ return;
+
+ // Adjust sidebar view.
+ [self showSidebarContents:sidebarContents];
+
+ // Notify extensions.
+ SidebarManager::GetInstance()->NotifyStateChanges(
+ oldSidebarContents, sidebarContents);
+}
+
+- (void)ensureContentsVisible {
+ [contentsController_ ensureContentsVisible];
+}
+
+- (void)showSidebarContents:(TabContents*)sidebarContents {
+ [contentsController_ ensureContentsSizeDoesNotChange];
+
+ NSArray* subviews = [splitView_ subviews];
+ if (sidebarContents) {
+ DCHECK_GE([subviews count], 1u);
+
+ // Native view is a TabContentsViewCocoa object, whose ViewID was
+ // set to VIEW_ID_TAB_CONTAINER initially, so change it to
+ // VIEW_ID_SIDE_BAR_CONTAINER here.
+ view_id_util::SetID(
+ sidebarContents->GetNativeView(), VIEW_ID_SIDE_BAR_CONTAINER);
+
+ CGFloat sidebarWidth = 0;
+ if ([subviews count] == 1) {
+ // Load the default split offset.
+ sidebarWidth = g_browser_process->local_state()->GetInteger(
+ prefs::kExtensionSidebarWidth);
+ if (sidebarWidth < 0) {
+ // Initial load, set to default value.
+ sidebarWidth =
+ NSWidth([splitView_ frame]) * kDefaultSidebarWidthRatio;
+ }
+ [splitView_ addSubview:[contentsController_ view]];
+ } else {
+ DCHECK_EQ([subviews count], 2u);
+ sidebarWidth = NSWidth([[subviews objectAtIndex:1] frame]);
+ }
+
+ // Make sure |sidebarWidth| isn't too large or too small.
+ sidebarWidth = std::min(sidebarWidth,
+ NSWidth([splitView_ frame]) - kMinWebWidth);
+ DCHECK_GE(sidebarWidth, 0) << "kMinWebWidth needs to be smaller than "
+ << "smallest available tab contents space.";
+ sidebarWidth = std::max(static_cast<CGFloat>(0), sidebarWidth);
+
+ [self resizeSidebarToNewWidth:sidebarWidth];
+ } else {
+ if ([subviews count] > 1) {
+ NSView* oldSidebarContentsView = [subviews objectAtIndex:1];
+ // Store split offset when hiding sidebar window only.
+ int sidebarWidth = NSWidth([oldSidebarContentsView frame]);
+ g_browser_process->local_state()->SetInteger(
+ prefs::kExtensionSidebarWidth, sidebarWidth);
+ [oldSidebarContentsView removeFromSuperview];
+ [splitView_ adjustSubviews];
+ }
+ }
+
+ [contentsController_ changeTabContents:sidebarContents];
+}
+
+- (void)resizeSidebarToNewWidth:(CGFloat)width {
+ NSArray* subviews = [splitView_ subviews];
+
+ // It seems as if |-setPosition:ofDividerAtIndex:| should do what's needed,
+ // but I can't figure out how to use it. Manually resize web and sidebar.
+ // TODO(alekseys): either make setPosition:ofDividerAtIndex: work or to add a
+ // category on NSSplitView to handle manual resizing.
+ NSView* sidebarView = [subviews objectAtIndex:1];
+ NSRect sidebarFrame = [sidebarView frame];
+ sidebarFrame.size.width = width;
+ [sidebarView setFrame:sidebarFrame];
+
+ NSView* webView = [subviews objectAtIndex:0];
+ NSRect webFrame = [webView frame];
+ webFrame.size.width =
+ NSWidth([splitView_ frame]) - ([splitView_ dividerThickness] + width);
+ [webView setFrame:webFrame];
+
+ [splitView_ adjustSubviews];
+}
+
+// NSSplitViewDelegate protocol.
+- (BOOL)splitView:(NSSplitView *)splitView
+ shouldAdjustSizeOfSubview:(NSView *)subview {
+ // Return NO for the sidebar view to indicate that it should not be resized
+ // automatically. The sidebar keeps the width set by the user.
+ if ([[splitView_ subviews] indexOfObject:subview] == 1)
+ return NO;
+ return YES;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/simple_content_exceptions_window_controller.h b/chrome/browser/ui/cocoa/simple_content_exceptions_window_controller.h
new file mode 100644
index 0000000..13daf99
--- /dev/null
+++ b/chrome/browser/ui/cocoa/simple_content_exceptions_window_controller.h
@@ -0,0 +1,38 @@
+// 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/cocoa_protocols_mac.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/remove_rows_table_model.h"
+#import "chrome/browser/ui/cocoa/table_model_array_controller.h"
+
+class RemoveRowsObserverBridge;
+
+// Controller for the geolocation exception dialog.
+@interface SimpleContentExceptionsWindowController : NSWindowController
+ <NSWindowDelegate> {
+ @private
+ IBOutlet NSTableView* tableView_;
+ IBOutlet NSButton* removeButton_;
+ IBOutlet NSButton* removeAllButton_;
+ IBOutlet NSButton* doneButton_;
+ IBOutlet TableModelArrayController* arrayController_;
+
+ scoped_ptr<RemoveRowsTableModel> model_;
+}
+
+// Shows or makes frontmost the exceptions window.
+// Changes made by the user in the window are persisted in |model|.
+// Takes ownership of |model|.
++ (id)controllerWithTableModel:(RemoveRowsTableModel*)model;
+
+// Sets the minimum width of the sheet and resizes it if necessary.
+- (void)setMinWidth:(CGFloat)minWidth;
+
+- (void)attachSheetTo:(NSWindow*)window;
+- (IBAction)closeSheet:(id)sender;
+
+@end
diff --git a/chrome/browser/ui/cocoa/simple_content_exceptions_window_controller.mm b/chrome/browser/ui/cocoa/simple_content_exceptions_window_controller.mm
new file mode 100644
index 0000000..7beac9c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/simple_content_exceptions_window_controller.mm
@@ -0,0 +1,125 @@
+// 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/ui/cocoa/simple_content_exceptions_window_controller.h"
+
+#include "app/l10n_util_mac.h"
+#include "app/table_model_observer.h"
+#include "base/logging.h"
+#import "base/mac_util.h"
+#import "base/scoped_nsobject.h"
+#include "base/sys_string_conversions.h"
+#include "grit/generated_resources.h"
+#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+@interface SimpleContentExceptionsWindowController (Private)
+- (id)initWithTableModel:(RemoveRowsTableModel*)model;
+@end
+
+namespace {
+
+const CGFloat kButtonBarHeight = 35.0;
+
+SimpleContentExceptionsWindowController* g_exceptionWindow = nil;
+
+} // namespace
+
+@implementation SimpleContentExceptionsWindowController
+
++ (id)controllerWithTableModel:(RemoveRowsTableModel*)model {
+ if (!g_exceptionWindow) {
+ g_exceptionWindow = [[SimpleContentExceptionsWindowController alloc]
+ initWithTableModel:model];
+ }
+ return g_exceptionWindow;
+}
+
+- (id)initWithTableModel:(RemoveRowsTableModel*)model {
+ NSString* nibpath = [mac_util::MainAppBundle()
+ pathForResource:@"SimpleContentExceptionsWindow"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ model_.reset(model);
+
+ // TODO(thakis): autoremember window rect.
+ // TODO(thakis): sorting support.
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ DCHECK([self window]);
+ DCHECK_EQ(self, [[self window] delegate]);
+ DCHECK(tableView_);
+ DCHECK(arrayController_);
+
+ CGFloat minWidth = [[removeButton_ superview] bounds].size.width +
+ [[doneButton_ superview] bounds].size.width;
+ [[self window] setMinSize:NSMakeSize(minWidth,
+ [[self window] minSize].height)];
+ NSDictionary* columns = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithInt:IDS_EXCEPTIONS_HOSTNAME_HEADER], @"hostname",
+ [NSNumber numberWithInt:IDS_EXCEPTIONS_ACTION_HEADER], @"action",
+ nil];
+ [arrayController_ bindToTableModel:model_.get()
+ withColumns:columns
+ groupTitleColumn:@"hostname"];
+}
+
+- (void)setMinWidth:(CGFloat)minWidth {
+ NSWindow* window = [self window];
+ [window setMinSize:NSMakeSize(minWidth, [window minSize].height)];
+ if ([window frame].size.width < minWidth) {
+ NSRect frame = [window frame];
+ frame.size.width = minWidth;
+ [window setFrame:frame display:NO];
+ }
+}
+
+- (void)windowWillClose:(NSNotification*)notification {
+ g_exceptionWindow = nil;
+ [self autorelease];
+}
+
+// Let esc close the window.
+- (void)cancel:(id)sender {
+ [self closeSheet:self];
+}
+
+- (void)keyDown:(NSEvent*)event {
+ NSString* chars = [event charactersIgnoringModifiers];
+ if ([chars length] == 1) {
+ switch ([chars characterAtIndex:0]) {
+ case NSDeleteCharacter:
+ case NSDeleteFunctionKey:
+ // Delete deletes.
+ if ([[tableView_ selectedRowIndexes] count] > 0)
+ [arrayController_ remove:event];
+ return;
+ }
+ }
+ [super keyDown:event];
+}
+
+- (void)attachSheetTo:(NSWindow*)window {
+ [NSApp beginSheet:[self window]
+ modalForWindow:window
+ modalDelegate:self
+ didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
+ contextInfo:nil];
+}
+
+- (void)sheetDidEnd:(NSWindow*)sheet
+ returnCode:(NSInteger)returnCode
+ contextInfo:(void*)context {
+ [sheet close];
+ [sheet orderOut:self];
+}
+
+- (IBAction)closeSheet:(id)sender {
+ [NSApp endSheet:[self window]];
+}
+
+
+@end
diff --git a/chrome/browser/ui/cocoa/simple_content_exceptions_window_controller_unittest.mm b/chrome/browser/ui/cocoa/simple_content_exceptions_window_controller_unittest.mm
new file mode 100644
index 0000000..58d1c84
--- /dev/null
+++ b/chrome/browser/ui/cocoa/simple_content_exceptions_window_controller_unittest.mm
@@ -0,0 +1,94 @@
+// 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/ui/cocoa/simple_content_exceptions_window_controller.h"
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/scoped_nsobject.h"
+#include "base/ref_counted.h"
+#include "chrome/browser/content_settings/host_content_settings_map.h"
+#include "chrome/browser/geolocation/geolocation_exceptions_table_model.h"
+#include "chrome/browser/plugin_exceptions_table_model.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface SimpleContentExceptionsWindowController (Testing)
+
+@property(readonly, nonatomic) TableModelArrayController* arrayController;
+
+@end
+
+@implementation SimpleContentExceptionsWindowController (Testing)
+
+- (TableModelArrayController*)arrayController {
+ return arrayController_;
+}
+
+@end
+
+
+namespace {
+
+class SimpleContentExceptionsWindowControllerTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ TestingProfile* profile = browser_helper_.profile();
+ geolocation_settings_ = new GeolocationContentSettingsMap(profile);
+ content_settings_ = new HostContentSettingsMap(profile);
+ }
+
+ SimpleContentExceptionsWindowController* GetController() {
+ GeolocationExceptionsTableModel* model = // Freed by window controller.
+ new GeolocationExceptionsTableModel(geolocation_settings_.get());
+ id controller = [SimpleContentExceptionsWindowController
+ controllerWithTableModel:model];
+ [controller showWindow:nil];
+ return controller;
+ }
+
+ void ClickRemoveAll(SimpleContentExceptionsWindowController* controller) {
+ [controller.arrayController removeAll:nil];
+ }
+
+ protected:
+ BrowserTestHelper browser_helper_;
+ scoped_refptr<GeolocationContentSettingsMap> geolocation_settings_;
+ scoped_refptr<HostContentSettingsMap> content_settings_;
+};
+
+TEST_F(SimpleContentExceptionsWindowControllerTest, Construction) {
+ GeolocationExceptionsTableModel* model = // Freed by window controller.
+ new GeolocationExceptionsTableModel(geolocation_settings_.get());
+ SimpleContentExceptionsWindowController* controller =
+ [SimpleContentExceptionsWindowController controllerWithTableModel:model];
+ [controller showWindow:nil];
+ [controller close]; // Should autorelease.
+}
+
+TEST_F(SimpleContentExceptionsWindowControllerTest, ShowPluginExceptions) {
+ PluginExceptionsTableModel* model = // Freed by window controller.
+ new PluginExceptionsTableModel(content_settings_.get(), NULL);
+ SimpleContentExceptionsWindowController* controller =
+ [SimpleContentExceptionsWindowController controllerWithTableModel:model];
+ [controller showWindow:nil];
+ [controller close]; // Should autorelease.
+}
+
+TEST_F(SimpleContentExceptionsWindowControllerTest, AddExistingEditAdd) {
+ geolocation_settings_->SetContentSetting(
+ GURL("http://myhost"), GURL(), CONTENT_SETTING_BLOCK);
+
+ SimpleContentExceptionsWindowController* controller = GetController();
+ ClickRemoveAll(controller);
+
+ [controller close];
+
+ EXPECT_EQ(0u, geolocation_settings_->GetAllOriginsSettings().size());
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/speech_input_window_controller.h b/chrome/browser/ui/cocoa/speech_input_window_controller.h
new file mode 100644
index 0000000..d68a30c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/speech_input_window_controller.h
@@ -0,0 +1,57 @@
+// 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_UI_COCOA_SPEECH_INPUT_WINDOW_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_SPEECH_INPUT_WINDOW_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/speech/speech_input_bubble.h"
+#include "chrome/browser/ui/cocoa/base_bubble_controller.h"
+
+// Controller for the speech input bubble window. This bubble window gets
+// displayed when the user starts speech input in a html input element.
+@interface SpeechInputWindowController : BaseBubbleController {
+ @private
+ SpeechInputBubble::Delegate* delegate_; // weak.
+
+ // References below are weak, being obtained from the nib.
+ IBOutlet NSImageView* iconImage_;
+ IBOutlet NSTextField* instructionLabel_;
+ IBOutlet NSButton* cancelButton_;
+ IBOutlet NSButton* tryAgainButton_;
+}
+
+// Initialize the window. |anchoredAt| is in screen coordinates.
+- (id)initWithParentWindow:(NSWindow*)parentWindow
+ delegate:(SpeechInputBubbleDelegate*)delegate
+ anchoredAt:(NSPoint)anchoredAt;
+
+// Handler for the cancel button.
+- (IBAction)cancel:(id)sender;
+
+// Handler for the try again button.
+- (IBAction)tryAgain:(id)sender;
+
+// Updates the UI with data related to the given display mode.
+- (void)updateLayout:(SpeechInputBubbleBase::DisplayMode)mode
+ messageText:(const string16&)messageText;
+
+// Makes the speech input bubble visible on screen.
+- (void)show;
+
+// Hides the speech input bubble away from screen. This does NOT release the
+// controller and the window.
+- (void)hide;
+
+// Sets the image to be displayed in the bubble's status ImageView. A future
+// call to updateLayout may change the image.
+// TODO(satish): Clean that up and move it into the platform independent
+// SpeechInputBubbleBase class.
+- (void)setImage:(NSImage*)image;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_SPEECH_INPUT_WINDOW_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/speech_input_window_controller.mm b/chrome/browser/ui/cocoa/speech_input_window_controller.mm
new file mode 100644
index 0000000..c490bc1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/speech_input_window_controller.mm
@@ -0,0 +1,188 @@
+// 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 "speech_input_window_controller.h"
+
+#include "app/l10n_util_mac.h"
+#include "app/resource_bundle.h"
+#include "base/logging.h"
+#include "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+
+#include "chrome/browser/ui/cocoa/info_bubble_view.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#import "skia/ext/skia_utils_mac.h"
+#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+const int kBubbleControlVerticalSpacing = 10; // Space between controls.
+const int kBubbleHorizontalMargin = 5; // Space on either sides of controls.
+
+@interface SpeechInputWindowController (Private)
+- (NSSize)calculateContentSize;
+- (void)layout:(NSSize)size;
+@end
+
+@implementation SpeechInputWindowController
+
+- (id)initWithParentWindow:(NSWindow*)parentWindow
+ delegate:(SpeechInputBubbleDelegate*)delegate
+ anchoredAt:(NSPoint)anchoredAt {
+ anchoredAt.y += info_bubble::kBubbleArrowHeight / 2.0;
+ if ((self = [super initWithWindowNibPath:@"SpeechInputBubble"
+ parentWindow:parentWindow
+ anchoredAt:anchoredAt])) {
+ DCHECK(delegate);
+ delegate_ = delegate;
+
+ [self showWindow:nil];
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ [super awakeFromNib];
+
+ NSWindow* window = [self window];
+ [[self bubble] setArrowLocation:info_bubble::kTopLeft];
+
+ NSSize newSize = [self calculateContentSize];
+ [[self bubble] setFrameSize:newSize];
+ NSSize windowDelta = NSMakeSize(
+ newSize.width - NSWidth([[window contentView] bounds]),
+ newSize.height - NSHeight([[window contentView] bounds]));
+ windowDelta = [[window contentView] convertSize:windowDelta toView:nil];
+ NSRect newFrame = [window frame];
+ newFrame.size.width += windowDelta.width;
+ newFrame.size.height += windowDelta.height;
+ [window setFrame:newFrame display:NO];
+
+ [self layout:newSize]; // Layout all the child controls.
+}
+
+- (IBAction)cancel:(id)sender {
+ delegate_->InfoBubbleButtonClicked(SpeechInputBubble::BUTTON_CANCEL);
+}
+
+- (IBAction)tryAgain:(id)sender {
+ delegate_->InfoBubbleButtonClicked(SpeechInputBubble::BUTTON_TRY_AGAIN);
+}
+
+// Calculate the window dimensions to reflect the sum height and max width of
+// all controls, with appropriate spacing between and around them. The returned
+// size is in view coordinates.
+- (NSSize)calculateContentSize {
+ [GTMUILocalizerAndLayoutTweaker sizeToFitView:cancelButton_];
+ [GTMUILocalizerAndLayoutTweaker sizeToFitView:tryAgainButton_];
+ NSSize cancelSize = [cancelButton_ bounds].size;
+ NSSize tryAgainSize = [tryAgainButton_ bounds].size;
+ int newHeight = cancelSize.height + kBubbleControlVerticalSpacing;
+ int newWidth = cancelSize.width + tryAgainSize.width;
+
+ if (![iconImage_ isHidden]) {
+ NSImage* icon = ResourceBundle::GetSharedInstance().GetNativeImageNamed(
+ IDR_SPEECH_INPUT_MIC_EMPTY);
+ NSSize size = [icon size];
+ newHeight += size.height + kBubbleControlVerticalSpacing;
+ if (newWidth < size.width)
+ newWidth = size.width;
+ } else {
+ newHeight += kBubbleControlVerticalSpacing;
+ }
+
+ [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:
+ instructionLabel_];
+ NSSize size = [instructionLabel_ bounds].size;
+ newHeight += size.height;
+ if (newWidth < size.width)
+ newWidth = size.width;
+
+ return NSMakeSize(newWidth + 2 * kBubbleHorizontalMargin,
+ newHeight + 2 * kBubbleControlVerticalSpacing);
+}
+
+// Position the controls within the given content area bounds.
+- (void)layout:(NSSize)size {
+ int y = kBubbleControlVerticalSpacing;
+
+ NSRect cancelRect = [cancelButton_ bounds];
+
+ if ([tryAgainButton_ isHidden]) {
+ cancelRect.origin.x = (size.width - NSWidth(cancelRect)) / 2;
+ } else {
+ NSRect tryAgainRect = [tryAgainButton_ bounds];
+ cancelRect.origin.x = (size.width - NSWidth(cancelRect) -
+ NSWidth(tryAgainRect)) / 2;
+ tryAgainRect.origin.x = cancelRect.origin.x + NSWidth(cancelRect);
+ tryAgainRect.origin.y = y;
+ [tryAgainButton_ setFrame:tryAgainRect];
+ }
+ cancelRect.origin.y = y;
+ [cancelButton_ setFrame:cancelRect];
+
+ y += NSHeight(cancelRect) + kBubbleControlVerticalSpacing;
+
+ NSRect rect;
+ if (![iconImage_ isHidden]) {
+ rect = [iconImage_ bounds];
+ rect.origin.x = (size.width - NSWidth(rect)) / 2;
+ rect.origin.y = y;
+ [iconImage_ setFrame:rect];
+ y += rect.size.height + kBubbleControlVerticalSpacing;
+ }
+
+ rect = [instructionLabel_ bounds];
+ rect.origin.x = (size.width - NSWidth(rect)) / 2;
+ rect.origin.y = y;
+ [instructionLabel_ setFrame:rect];
+}
+
+- (void)updateLayout:(SpeechInputBubbleBase::DisplayMode)mode
+ messageText:(const string16&)messageText {
+ // Get the right set of controls to be visible.
+ if (mode == SpeechInputBubbleBase::DISPLAY_MODE_MESSAGE) {
+ [instructionLabel_ setStringValue:base::SysUTF16ToNSString(messageText)];
+ [iconImage_ setHidden:YES];
+ [tryAgainButton_ setHidden:NO];
+ } else {
+ if (mode == SpeechInputBubbleBase::DISPLAY_MODE_RECORDING) {
+ [instructionLabel_ setStringValue:l10n_util::GetNSString(
+ IDS_SPEECH_INPUT_BUBBLE_HEADING)];
+ NSImage* icon = ResourceBundle::GetSharedInstance().GetNativeImageNamed(
+ IDR_SPEECH_INPUT_MIC_EMPTY);
+ [iconImage_ setImage:icon];
+ } else {
+ [instructionLabel_ setStringValue:l10n_util::GetNSString(
+ IDS_SPEECH_INPUT_BUBBLE_WORKING)];
+ }
+ [iconImage_ setHidden:NO];
+ [iconImage_ setNeedsDisplay:YES];
+ [tryAgainButton_ setHidden:YES];
+ }
+
+ NSSize newSize = [self calculateContentSize];
+ NSRect rect = [[self bubble] frame];
+ rect.origin.y -= newSize.height - rect.size.height;
+ rect.size = newSize;
+ [[self bubble] setFrame:rect];
+ [self layout:newSize];
+}
+
+- (void)windowWillClose:(NSNotification*)notification {
+ delegate_->InfoBubbleFocusChanged();
+}
+
+- (void)show {
+ [self showWindow:nil];
+}
+
+- (void)hide {
+ [[self window] orderOut:nil];
+}
+
+- (void)setImage:(NSImage*)image {
+ [iconImage_ setImage:image];
+}
+
+@end // implementation SpeechInputWindowController
diff --git a/chrome/browser/ui/cocoa/ssl_client_certificate_selector.mm b/chrome/browser/ui/cocoa/ssl_client_certificate_selector.mm
new file mode 100644
index 0000000..3f0a2f5
--- /dev/null
+++ b/chrome/browser/ui/cocoa/ssl_client_certificate_selector.mm
@@ -0,0 +1,195 @@
+// 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 "chrome/browser/ssl_client_certificate_selector.h"
+
+#import <SecurityInterface/SFChooseIdentityPanel.h>
+
+#include <vector>
+
+#import "app/l10n_util_mac.h"
+#include "base/logging.h"
+#include "base/ref_counted.h"
+#import "base/scoped_nsobject.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/browser_thread.h"
+#include "chrome/browser/ssl/ssl_client_auth_handler.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#import "chrome/browser/ui/cocoa/constrained_window_mac.h"
+#include "grit/generated_resources.h"
+#include "net/base/x509_certificate.h"
+
+namespace {
+
+class ConstrainedSFChooseIdentityPanel
+ : public ConstrainedWindowMacDelegateSystemSheet {
+ public:
+ ConstrainedSFChooseIdentityPanel(SFChooseIdentityPanel* panel,
+ id delegate, SEL didEndSelector,
+ NSArray* identities, NSString* message)
+ : ConstrainedWindowMacDelegateSystemSheet(delegate, didEndSelector),
+ identities_([identities retain]),
+ message_([message retain]) {
+ set_sheet(panel);
+ }
+
+ virtual ~ConstrainedSFChooseIdentityPanel() {
+ // As required by ConstrainedWindowMacDelegate, close the sheet if
+ // it's still open.
+ if (is_sheet_open()) {
+ [NSApp endSheet:sheet()
+ returnCode:NSFileHandlingPanelCancelButton];
+ }
+ }
+
+ // ConstrainedWindowMacDelegateSystemSheet implementation:
+ virtual void DeleteDelegate() {
+ delete this;
+ }
+
+ // SFChooseIdentityPanel's beginSheetForWindow: method has more arguments
+ // than the usual one. Also pass the panel through contextInfo argument
+ // because the callback has the wrong signature.
+ virtual NSArray* GetSheetParameters(id delegate, SEL didEndSelector) {
+ return [NSArray arrayWithObjects:
+ [NSNull null], // window, must be [NSNull null]
+ delegate,
+ [NSValue valueWithPointer:didEndSelector],
+ [NSValue valueWithPointer:sheet()],
+ identities_.get(),
+ message_.get(),
+ nil];
+ }
+
+ private:
+ scoped_nsobject<NSArray> identities_;
+ scoped_nsobject<NSString> message_;
+ DISALLOW_COPY_AND_ASSIGN(ConstrainedSFChooseIdentityPanel);
+};
+
+} // namespace
+
+@interface SSLClientCertificateSelectorCocoa : NSObject {
+ @private
+ // The handler to report back to.
+ scoped_refptr<SSLClientAuthHandler> handler_;
+ // The certificate request we serve.
+ scoped_refptr<net::SSLCertRequestInfo> certRequestInfo_;
+ // The list of identities offered to the user.
+ scoped_nsobject<NSMutableArray> identities_;
+ // The corresponding list of certificates.
+ std::vector<scoped_refptr<net::X509Certificate> > certificates_;
+ // The currently open dialog.
+ ConstrainedWindow* window_;
+}
+
+- (id)initWithHandler:(SSLClientAuthHandler*)handler
+ certRequestInfo:(net::SSLCertRequestInfo*)certRequestInfo;
+- (void)displayDialog:(TabContents*)parent;
+@end
+
+namespace browser {
+
+void ShowSSLClientCertificateSelector(
+ TabContents* parent,
+ net::SSLCertRequestInfo* cert_request_info,
+ SSLClientAuthHandler* delegate) {
+ // TODO(davidben): Implement a tab-modal dialog.
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+ SSLClientCertificateSelectorCocoa* selector =
+ [[[SSLClientCertificateSelectorCocoa alloc]
+ initWithHandler:delegate
+ certRequestInfo:cert_request_info] autorelease];
+ [selector displayDialog:parent];
+}
+
+} // namespace browser
+
+@implementation SSLClientCertificateSelectorCocoa
+
+- (id)initWithHandler:(SSLClientAuthHandler*)handler
+ certRequestInfo:(net::SSLCertRequestInfo*)certRequestInfo {
+ DCHECK(handler);
+ DCHECK(certRequestInfo);
+ if ((self = [super init])) {
+ handler_ = handler;
+ certRequestInfo_ = certRequestInfo;
+ window_ = NULL;
+ }
+ return self;
+}
+
+- (void)sheetDidEnd:(NSWindow*)parent
+ returnCode:(NSInteger)returnCode
+ context:(void*)context {
+ DCHECK(context);
+ SFChooseIdentityPanel* panel = static_cast<SFChooseIdentityPanel*>(context);
+
+ net::X509Certificate* cert = NULL;
+ if (returnCode == NSFileHandlingPanelOKButton) {
+ NSUInteger index = [identities_ indexOfObject:(id)[panel identity]];
+ if (index != NSNotFound)
+ cert = certificates_[index];
+ else
+ NOTREACHED();
+ }
+
+ // Finally, tell the backend which identity (or none) the user selected.
+ handler_->CertificateSelected(cert);
+ // Close the constrained window.
+ DCHECK(window_);
+ window_->CloseConstrainedWindow();
+
+ // Now that the panel has closed, release it. Note that the autorelease is
+ // needed. After this callback returns, the panel is still accessed, so a
+ // normal release crashes.
+ [panel autorelease];
+}
+
+- (void)displayDialog:(TabContents*)parent {
+ DCHECK(!window_);
+ // Create an array of CFIdentityRefs for the certificates:
+ size_t numCerts = certRequestInfo_->client_certs.size();
+ identities_.reset([[NSMutableArray alloc] initWithCapacity:numCerts]);
+ for (size_t i = 0; i < numCerts; ++i) {
+ SecCertificateRef cert;
+ cert = certRequestInfo_->client_certs[i]->os_cert_handle();
+ SecIdentityRef identity;
+ if (SecIdentityCreateWithCertificate(NULL, cert, &identity) == noErr) {
+ [identities_ addObject:(id)identity];
+ CFRelease(identity);
+ certificates_.push_back(certRequestInfo_->client_certs[i]);
+ }
+ }
+
+ // Get the message to display:
+ NSString* title = l10n_util::GetNSString(IDS_CLIENT_CERT_DIALOG_TITLE);
+ NSString* message = l10n_util::GetNSStringF(
+ IDS_CLIENT_CERT_DIALOG_TEXT,
+ ASCIIToUTF16(certRequestInfo_->host_and_port));
+
+ // Create and set up a system choose-identity panel.
+ SFChooseIdentityPanel* panel = [[SFChooseIdentityPanel alloc] init];
+ [panel setInformativeText:message];
+ [panel setDefaultButtonTitle:l10n_util::GetNSString(IDS_OK)];
+ [panel setAlternateButtonTitle:l10n_util::GetNSString(IDS_CANCEL)];
+ SecPolicyRef sslPolicy;
+ if (net::X509Certificate::CreateSSLClientPolicy(&sslPolicy) == noErr) {
+ [panel setPolicies:(id)sslPolicy];
+ CFRelease(sslPolicy);
+ }
+
+ window_ =
+ parent->CreateConstrainedDialog(new ConstrainedSFChooseIdentityPanel(
+ panel, self,
+ @selector(sheetDidEnd:returnCode:context:),
+ identities_, title));
+ // Note: SFChooseIdentityPanel does not take a reference to itself while the
+ // sheet is open. Don't release the ownership claim until the sheet has ended
+ // in |-sheetDidEnd:returnCode:context:|.
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/status_bubble_mac.h b/chrome/browser/ui/cocoa/status_bubble_mac.h
new file mode 100644
index 0000000..d32f67c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/status_bubble_mac.h
@@ -0,0 +1,172 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_STATUS_BUBBLE_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_STATUS_BUBBLE_MAC_H_
+#pragma once
+
+#include <string>
+
+#import <Cocoa/Cocoa.h>
+#import <QuartzCore/QuartzCore.h>
+
+#include "base/string16.h"
+#include "base/task.h"
+#include "chrome/browser/status_bubble.h"
+#include "googleurl/src/gurl.h"
+
+class GURL;
+class StatusBubbleMacTest;
+
+class StatusBubbleMac : public StatusBubble {
+ public:
+ // The various states that a status bubble may be in. Public for delegate
+ // access (for testing).
+ enum StatusBubbleState {
+ kBubbleHidden, // Fully hidden
+ kBubbleShowingTimer, // Waiting to fade in
+ kBubbleShowingFadeIn, // In a fade-in transition
+ kBubbleShown, // Fully visible
+ kBubbleHidingTimer, // Waiting to fade out
+ kBubbleHidingFadeOut // In a fade-out transition
+ };
+
+ StatusBubbleMac(NSWindow* parent, id delegate);
+ virtual ~StatusBubbleMac();
+
+ // StatusBubble implementation.
+ virtual void SetStatus(const string16& status);
+ virtual void SetURL(const GURL& url, const string16& languages);
+ virtual void Hide();
+ virtual void MouseMoved(const gfx::Point& location, bool left_content);
+ virtual void UpdateDownloadShelfVisibility(bool visible);
+
+ // Mac-specific method: Update the size and position of the status bubble to
+ // match the parent window. Safe to call even when the status bubble does not
+ // exist.
+ void UpdateSizeAndPosition();
+
+ // Mac-specific method: Change the parent window of the status bubble. Safe to
+ // call even when the status bubble does not exist.
+ void SwitchParentWindow(NSWindow* parent);
+
+ // Delegate method called when a fade-in or fade-out transition has
+ // completed. This is public so that it may be visible to the CAAnimation
+ // delegate, which is an Objective-C object.
+ void AnimationDidStop(CAAnimation* animation, bool finished);
+
+ // Expand the bubble to fit a URL too long for the standard bubble size.
+ void ExpandBubble();
+
+ private:
+ friend class StatusBubbleMacTest;
+
+ // Setter for state_. Use this instead of writing to state_ directly so
+ // that state changes can be observed by unit tests.
+ void SetState(StatusBubbleState state);
+
+ // Sets the bubble text for SetStatus and SetURL.
+ void SetText(const string16& text, bool is_url);
+
+ // Construct the window/widget if it does not already exist. (Safe to call if
+ // it does.)
+ void Create();
+
+ // Attaches the status bubble window to its parent window. Safe to call even
+ // when already attached.
+ void Attach();
+
+ // Detaches the status bubble window from its parent window.
+ void Detach();
+
+ // Is the status bubble attached to the browser window? It should be attached
+ // when shown and during any fades, but should be detached when hidden.
+ bool is_attached() { return [window_ parentWindow] != nil; }
+
+ // Begins fading the status bubble window in or out depending on the value
+ // of |show|. This must be called from the appropriate fade state,
+ // kBubbleShowingFadeIn or kBubbleHidingFadeOut, or from the appropriate
+ // fully-shown/hidden state, kBubbleShown or kBubbleHidden. This may be
+ // called at any point during a fade-in or fade-out; it is even possible to
+ // reverse a transition before it has completed.
+ void Fade(bool show);
+
+ // One-shot timer operations to manage the delays associated with the
+ // kBubbleShowingTimer and kBubbleHidingTimer states. StartTimer and
+ // TimerFired must be called from one of these states. StartTimer may be
+ // called while the timer is still running; in that case, the timer will be
+ // reset. CancelTimer may be called from any state.
+ void StartTimer(int64 time_ms);
+ void CancelTimer();
+ void TimerFired();
+
+ // Begin the process of showing or hiding the status bubble. These may be
+ // called from any state, and will take the appropriate action to initiate
+ // any state changes that may be needed.
+ void StartShowing();
+ void StartHiding();
+
+ // Cancel the expansion timer.
+ void CancelExpandTimer();
+
+ // The timer factory used for show and hide delay timers.
+ ScopedRunnableMethodFactory<StatusBubbleMac> timer_factory_;
+
+ // The timer factory used for the expansion delay timer.
+ ScopedRunnableMethodFactory<StatusBubbleMac> expand_timer_factory_;
+
+ // Calculate the appropriate frame for the status bubble window. If
+ // |expanded_width|, use entire width of parent frame.
+ NSRect CalculateWindowFrame(bool expanded_width);
+
+ // The window we attach ourselves to.
+ NSWindow* parent_; // WEAK
+
+ // The object that we query about our vertical offset for positioning.
+ id delegate_; // WEAK
+
+ // The window we own.
+ NSWindow* window_;
+
+ // The status text we want to display when there are no URLs to display.
+ NSString* status_text_;
+
+ // The url we want to display when there is no status text to display.
+ NSString* url_text_;
+
+ // The status bubble's current state. Do not write to this field directly;
+ // use SetState().
+ StatusBubbleState state_;
+
+ // True if operations are to be performed immediately rather than waiting
+ // for delays and transitions. Normally false, this should only be set to
+ // true for testing.
+ bool immediate_;
+
+ // True if the status bubble has been expanded. If the bubble is in the
+ // expanded state and encounters a new URL, change size immediately,
+ // with no hover delay.
+ bool is_expanded_;
+
+ // The original, non-elided URL.
+ GURL url_;
+
+ // Needs to be passed to ElideURL if the original URL string is wider than
+ // the standard bubble width.
+ string16 languages_;
+
+ DISALLOW_COPY_AND_ASSIGN(StatusBubbleMac);
+};
+
+// Delegate interface
+@interface NSObject(StatusBubbleDelegate)
+// Called to query the delegate about the frame StatusBubble should position
+// itself in. Frame is returned in the parent window coordinates.
+- (NSRect)statusBubbleBaseFrame;
+
+// Called from SetState to notify the delegate of state changes.
+- (void)statusBubbleWillEnterState:(StatusBubbleMac::StatusBubbleState)state;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_STATUS_BUBBLE_MAC_H_
diff --git a/chrome/browser/ui/cocoa/status_bubble_mac.mm b/chrome/browser/ui/cocoa/status_bubble_mac.mm
new file mode 100644
index 0000000..6231e5d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/status_bubble_mac.mm
@@ -0,0 +1,705 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/status_bubble_mac.h"
+
+#include <limits>
+
+#include "app/text_elider.h"
+#include "base/compiler_specific.h"
+#include "base/message_loop.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#import "chrome/browser/ui/cocoa/bubble_view.h"
+#include "gfx/point.h"
+#include "net/base/net_util.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+#import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h"
+#import "third_party/GTM/AppKit/GTMNSColor+Luminance.h"
+
+namespace {
+
+const int kWindowHeight = 18;
+
+// The width of the bubble in relation to the width of the parent window.
+const CGFloat kWindowWidthPercent = 1.0 / 3.0;
+
+// How close the mouse can get to the infobubble before it starts sliding
+// off-screen.
+const int kMousePadding = 20;
+
+const int kTextPadding = 3;
+
+// The animation key used for fade-in and fade-out transitions.
+NSString* const kFadeAnimationKey = @"alphaValue";
+
+// The status bubble's maximum opacity, when fully faded in.
+const CGFloat kBubbleOpacity = 1.0;
+
+// Delay before showing or hiding the bubble after a SetStatus or SetURL call.
+const int64 kShowDelayMilliseconds = 80;
+const int64 kHideDelayMilliseconds = 250;
+
+// How long each fade should last.
+const NSTimeInterval kShowFadeInDurationSeconds = 0.120;
+const NSTimeInterval kHideFadeOutDurationSeconds = 0.200;
+
+// The minimum representable time interval. This can be used as the value
+// passed to +[NSAnimationContext setDuration:] to stop an in-progress
+// animation as quickly as possible.
+const NSTimeInterval kMinimumTimeInterval =
+ std::numeric_limits<NSTimeInterval>::min();
+
+// How quickly the status bubble should expand, in seconds.
+const CGFloat kExpansionDuration = 0.125;
+
+} // namespace
+
+@interface StatusBubbleAnimationDelegate : NSObject {
+ @private
+ StatusBubbleMac* statusBubble_; // weak; owns us indirectly
+}
+
+- (id)initWithStatusBubble:(StatusBubbleMac*)statusBubble;
+
+// Invalidates this object so that no further calls will be made to
+// statusBubble_. This should be called when statusBubble_ is released, to
+// prevent attempts to call into the released object.
+- (void)invalidate;
+
+// CAAnimation delegate method
+- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished;
+@end
+
+@implementation StatusBubbleAnimationDelegate
+
+- (id)initWithStatusBubble:(StatusBubbleMac*)statusBubble {
+ if ((self = [super init])) {
+ statusBubble_ = statusBubble;
+ }
+
+ return self;
+}
+
+- (void)invalidate {
+ statusBubble_ = NULL;
+}
+
+- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
+ if (statusBubble_)
+ statusBubble_->AnimationDidStop(animation, finished ? true : false);
+}
+
+@end
+
+StatusBubbleMac::StatusBubbleMac(NSWindow* parent, id delegate)
+ : ALLOW_THIS_IN_INITIALIZER_LIST(timer_factory_(this)),
+ ALLOW_THIS_IN_INITIALIZER_LIST(expand_timer_factory_(this)),
+ parent_(parent),
+ delegate_(delegate),
+ window_(nil),
+ status_text_(nil),
+ url_text_(nil),
+ state_(kBubbleHidden),
+ immediate_(false),
+ is_expanded_(false) {
+}
+
+StatusBubbleMac::~StatusBubbleMac() {
+ Hide();
+
+ if (window_) {
+ [[[window_ animationForKey:kFadeAnimationKey] delegate] invalidate];
+ Detach();
+ [window_ release];
+ window_ = nil;
+ }
+}
+
+void StatusBubbleMac::SetStatus(const string16& status) {
+ Create();
+
+ SetText(status, false);
+}
+
+void StatusBubbleMac::SetURL(const GURL& url, const string16& languages) {
+ url_ = url;
+ languages_ = languages;
+
+ Create();
+
+ NSRect frame = [window_ frame];
+
+ // Reset frame size when bubble is hidden.
+ if (state_ == kBubbleHidden) {
+ is_expanded_ = false;
+ frame.size.width = NSWidth(CalculateWindowFrame(/*expand=*/false));
+ [window_ setFrame:frame display:NO];
+ }
+
+ int text_width = static_cast<int>(NSWidth(frame) -
+ kBubbleViewTextPositionX -
+ kTextPadding);
+
+ // Scale from view to window coordinates before eliding URL string.
+ NSSize scaled_width = NSMakeSize(text_width, 0);
+ scaled_width = [[parent_ contentView] convertSize:scaled_width fromView:nil];
+ text_width = static_cast<int>(scaled_width.width);
+ NSFont* font = [[window_ contentView] font];
+ gfx::Font font_chr(base::SysNSStringToWide([font fontName]),
+ [font pointSize]);
+
+ string16 original_url_text = net::FormatUrl(url, UTF16ToUTF8(languages));
+ string16 status = gfx::ElideUrl(url, font_chr, text_width,
+ UTF16ToWideHack(languages));
+
+ SetText(status, true);
+
+ // In testing, don't use animation. When ExpandBubble is tested, it is
+ // called explicitly.
+ if (immediate_)
+ return;
+ else
+ CancelExpandTimer();
+
+ // If the bubble has been expanded, the user has already hovered over a link
+ // to trigger the expanded state. Don't wait to change the bubble in this
+ // case -- immediately expand or contract to fit the URL.
+ if (is_expanded_ && !url.is_empty()) {
+ ExpandBubble();
+ } else if (original_url_text.length() > status.length()) {
+ MessageLoop::current()->PostDelayedTask(FROM_HERE,
+ expand_timer_factory_.NewRunnableMethod(
+ &StatusBubbleMac::ExpandBubble), kExpandHoverDelay);
+ }
+}
+
+void StatusBubbleMac::SetText(const string16& text, bool is_url) {
+ // The status bubble allows the status and URL strings to be set
+ // independently. Whichever was set non-empty most recently will be the
+ // value displayed. When both are empty, the status bubble hides.
+
+ NSString* text_ns = base::SysUTF16ToNSString(text);
+
+ NSString** main;
+ NSString** backup;
+
+ if (is_url) {
+ main = &url_text_;
+ backup = &status_text_;
+ } else {
+ main = &status_text_;
+ backup = &url_text_;
+ }
+
+ // Don't return from this function early. It's important to make sure that
+ // all calls to StartShowing and StartHiding are made, so that all delays
+ // are observed properly. Specifically, if the state is currently
+ // kBubbleShowingTimer, the timer will need to be restarted even if
+ // [text_ns isEqualToString:*main] is true.
+
+ [*main autorelease];
+ *main = [text_ns retain];
+
+ bool show = true;
+ if ([*main length] > 0)
+ [[window_ contentView] setContent:*main];
+ else if ([*backup length] > 0)
+ [[window_ contentView] setContent:*backup];
+ else
+ show = false;
+
+ if (show)
+ StartShowing();
+ else
+ StartHiding();
+}
+
+void StatusBubbleMac::Hide() {
+ CancelTimer();
+ CancelExpandTimer();
+ is_expanded_ = false;
+
+ bool fade_out = false;
+ if (state_ == kBubbleHidingFadeOut || state_ == kBubbleShowingFadeIn) {
+ SetState(kBubbleHidingFadeOut);
+
+ if (!immediate_) {
+ // An animation is in progress. Cancel it by starting a new animation.
+ // Use kMinimumTimeInterval to set the opacity as rapidly as possible.
+ fade_out = true;
+ [NSAnimationContext beginGrouping];
+ [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
+ [[window_ animator] setAlphaValue:0.0];
+ [NSAnimationContext endGrouping];
+ }
+ }
+
+ if (!fade_out) {
+ // No animation is in progress, so the opacity can be set directly.
+ [window_ setAlphaValue:0.0];
+ SetState(kBubbleHidden);
+ }
+
+ // Stop any width animation and reset the bubble size.
+ if (!immediate_) {
+ [NSAnimationContext beginGrouping];
+ [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
+ [[window_ animator] setFrame:CalculateWindowFrame(/*expand=*/false)
+ display:NO];
+ [NSAnimationContext endGrouping];
+ } else {
+ [window_ setFrame:CalculateWindowFrame(/*expand=*/false) display:NO];
+ }
+
+ [status_text_ release];
+ status_text_ = nil;
+ [url_text_ release];
+ url_text_ = nil;
+}
+
+void StatusBubbleMac::MouseMoved(
+ const gfx::Point& location, bool left_content) {
+ if (left_content)
+ return;
+
+ if (!window_)
+ return;
+
+ // TODO(thakis): Use 'location' here instead of NSEvent.
+ NSPoint cursor_location = [NSEvent mouseLocation];
+ --cursor_location.y; // docs say the y coord starts at 1 not 0; don't ask why
+
+ // Bubble's base frame in |parent_| coordinates.
+ NSRect baseFrame;
+ if ([delegate_ respondsToSelector:@selector(statusBubbleBaseFrame)])
+ baseFrame = [delegate_ statusBubbleBaseFrame];
+ else
+ baseFrame = [[parent_ contentView] frame];
+
+ // Get the normal position of the frame.
+ NSRect window_frame = [window_ frame];
+ window_frame.origin = [parent_ convertBaseToScreen:baseFrame.origin];
+
+ // Get the cursor position relative to the popup.
+ cursor_location.x -= NSMaxX(window_frame);
+ cursor_location.y -= NSMaxY(window_frame);
+
+
+ // If the mouse is in a position where we think it would move the
+ // status bubble, figure out where and how the bubble should be moved.
+ if (cursor_location.y < kMousePadding &&
+ cursor_location.x < kMousePadding) {
+ int offset = kMousePadding - cursor_location.y;
+
+ // Make the movement non-linear.
+ offset = offset * offset / kMousePadding;
+
+ // When the mouse is entering from the right, we want the offset to be
+ // scaled by how horizontally far away the cursor is from the bubble.
+ if (cursor_location.x > 0) {
+ offset = offset * ((kMousePadding - cursor_location.x) / kMousePadding);
+ }
+
+ bool isOnScreen = true;
+ NSScreen* screen = [window_ screen];
+ if (screen &&
+ NSMinY([screen visibleFrame]) > NSMinY(window_frame) - offset) {
+ isOnScreen = false;
+ }
+
+ // If something is shown below tab contents (devtools, download shelf etc.),
+ // adjust the position to sit on top of it.
+ bool isAnyShelfVisible = NSMinY(baseFrame) > 0;
+
+ if (isOnScreen && !isAnyShelfVisible) {
+ // Cap the offset and change the visual presentation of the bubble
+ // depending on where it ends up (so that rounded corners square off
+ // and mate to the edges of the tab content).
+ if (offset >= NSHeight(window_frame)) {
+ offset = NSHeight(window_frame);
+ [[window_ contentView] setCornerFlags:
+ kRoundedBottomLeftCorner | kRoundedBottomRightCorner];
+ } else if (offset > 0) {
+ [[window_ contentView] setCornerFlags:
+ kRoundedTopRightCorner | kRoundedBottomLeftCorner |
+ kRoundedBottomRightCorner];
+ } else {
+ [[window_ contentView] setCornerFlags:kRoundedTopRightCorner];
+ }
+ window_frame.origin.y -= offset;
+ } else {
+ // Cannot move the bubble down without obscuring other content.
+ // Move it to the right instead.
+ [[window_ contentView] setCornerFlags:kRoundedTopLeftCorner];
+
+ // Subtract border width + bubble width.
+ window_frame.origin.x += NSWidth(baseFrame) - NSWidth(window_frame);
+ }
+ } else {
+ [[window_ contentView] setCornerFlags:kRoundedTopRightCorner];
+ }
+
+ [window_ setFrame:window_frame display:YES];
+}
+
+void StatusBubbleMac::UpdateDownloadShelfVisibility(bool visible) {
+}
+
+void StatusBubbleMac::Create() {
+ if (window_)
+ return;
+
+ // TODO(avi):fix this for RTL
+ NSRect window_rect = CalculateWindowFrame(/*expand=*/false);
+ // initWithContentRect has origin in screen coords and size in scaled window
+ // coordinates.
+ window_rect.size =
+ [[parent_ contentView] convertSize:window_rect.size fromView:nil];
+ window_ = [[NSWindow alloc] initWithContentRect:window_rect
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:YES];
+ [window_ setMovableByWindowBackground:NO];
+ [window_ setBackgroundColor:[NSColor clearColor]];
+ [window_ setLevel:NSNormalWindowLevel];
+ [window_ setOpaque:NO];
+ [window_ setHasShadow:NO];
+
+ // We do not need to worry about the bubble outliving |parent_| because our
+ // teardown sequence in BWC guarantees that |parent_| outlives the status
+ // bubble and that the StatusBubble is torn down completely prior to the
+ // window going away.
+ scoped_nsobject<BubbleView> view(
+ [[BubbleView alloc] initWithFrame:NSZeroRect themeProvider:parent_]);
+ [window_ setContentView:view];
+
+ [window_ setAlphaValue:0.0];
+
+ // Set a delegate for the fade-in and fade-out transitions to be notified
+ // when fades are complete. The ownership model is for window_ to own
+ // animation_dictionary, which owns animation, which owns
+ // animation_delegate.
+ CAAnimation* animation = [[window_ animationForKey:kFadeAnimationKey] copy];
+ [animation autorelease];
+ StatusBubbleAnimationDelegate* animation_delegate =
+ [[StatusBubbleAnimationDelegate alloc] initWithStatusBubble:this];
+ [animation_delegate autorelease];
+ [animation setDelegate:animation_delegate];
+ NSMutableDictionary* animation_dictionary =
+ [NSMutableDictionary dictionaryWithDictionary:[window_ animations]];
+ [animation_dictionary setObject:animation forKey:kFadeAnimationKey];
+ [window_ setAnimations:animation_dictionary];
+
+ // Don't |Attach()| since we don't know the appropriate state; let the
+ // |SetState()| call do that.
+
+ [view setCornerFlags:kRoundedTopRightCorner];
+ MouseMoved(gfx::Point(), false);
+}
+
+void StatusBubbleMac::Attach() {
+ // This method may be called several times during the process of creating or
+ // showing a status bubble to attach the bubble to its parent window.
+ if (!is_attached()) {
+ [parent_ addChildWindow:window_ ordered:NSWindowAbove];
+ UpdateSizeAndPosition();
+ }
+}
+
+void StatusBubbleMac::Detach() {
+ // This method may be called several times in the process of hiding or
+ // destroying a status bubble.
+ if (is_attached()) {
+ // Magic setFrame: See crbug.com/58506, and codereview.chromium.org/3573014
+ [window_ setFrame:CalculateWindowFrame(/*expand=*/false) display:NO];
+ [parent_ removeChildWindow:window_]; // See crbug.com/28107 ...
+ [window_ orderOut:nil]; // ... and crbug.com/29054.
+ }
+}
+
+void StatusBubbleMac::AnimationDidStop(CAAnimation* animation, bool finished) {
+ DCHECK([NSThread isMainThread]);
+ DCHECK(state_ == kBubbleShowingFadeIn || state_ == kBubbleHidingFadeOut);
+ DCHECK(is_attached());
+
+ if (finished) {
+ // Because of the mechanism used to interrupt animations, this is never
+ // actually called with finished set to false. If animations ever become
+ // directly interruptible, the check will ensure that state_ remains
+ // properly synchronized.
+ if (state_ == kBubbleShowingFadeIn) {
+ DCHECK_EQ([[window_ animator] alphaValue], kBubbleOpacity);
+ SetState(kBubbleShown);
+ } else {
+ DCHECK_EQ([[window_ animator] alphaValue], 0.0);
+ SetState(kBubbleHidden);
+ }
+ }
+}
+
+void StatusBubbleMac::SetState(StatusBubbleState state) {
+ // We must be hidden or attached, but not both.
+ DCHECK((state_ == kBubbleHidden) ^ is_attached());
+
+ if (state == state_)
+ return;
+
+ if (state == kBubbleHidden)
+ Detach();
+ else
+ Attach();
+
+ if ([delegate_ respondsToSelector:@selector(statusBubbleWillEnterState:)])
+ [delegate_ statusBubbleWillEnterState:state];
+
+ state_ = state;
+}
+
+void StatusBubbleMac::Fade(bool show) {
+ DCHECK([NSThread isMainThread]);
+
+ StatusBubbleState fade_state = kBubbleShowingFadeIn;
+ StatusBubbleState target_state = kBubbleShown;
+ NSTimeInterval full_duration = kShowFadeInDurationSeconds;
+ CGFloat opacity = kBubbleOpacity;
+
+ if (!show) {
+ fade_state = kBubbleHidingFadeOut;
+ target_state = kBubbleHidden;
+ full_duration = kHideFadeOutDurationSeconds;
+ opacity = 0.0;
+ }
+
+ DCHECK(state_ == fade_state || state_ == target_state);
+
+ if (state_ == target_state)
+ return;
+
+ if (immediate_) {
+ [window_ setAlphaValue:opacity];
+ SetState(target_state);
+ return;
+ }
+
+ // If an incomplete transition has left the opacity somewhere between 0 and
+ // kBubbleOpacity, the fade rate is kept constant by shortening the duration.
+ NSTimeInterval duration =
+ full_duration *
+ fabs(opacity - [[window_ animator] alphaValue]) / kBubbleOpacity;
+
+ // 0.0 will not cancel an in-progress animation.
+ if (duration == 0.0)
+ duration = kMinimumTimeInterval;
+
+ // This will cancel an in-progress transition and replace it with this fade.
+ [NSAnimationContext beginGrouping];
+ // Don't use the GTM additon for the "Steve" slowdown because this can happen
+ // async from user actions and the effects could be a surprise.
+ [[NSAnimationContext currentContext] setDuration:duration];
+ [[window_ animator] setAlphaValue:opacity];
+ [NSAnimationContext endGrouping];
+}
+
+void StatusBubbleMac::StartTimer(int64 delay_ms) {
+ DCHECK([NSThread isMainThread]);
+ DCHECK(state_ == kBubbleShowingTimer || state_ == kBubbleHidingTimer);
+
+ if (immediate_) {
+ TimerFired();
+ return;
+ }
+
+ // There can only be one running timer.
+ CancelTimer();
+
+ MessageLoop::current()->PostDelayedTask(
+ FROM_HERE,
+ timer_factory_.NewRunnableMethod(&StatusBubbleMac::TimerFired),
+ delay_ms);
+}
+
+void StatusBubbleMac::CancelTimer() {
+ DCHECK([NSThread isMainThread]);
+
+ if (!timer_factory_.empty())
+ timer_factory_.RevokeAll();
+}
+
+void StatusBubbleMac::TimerFired() {
+ DCHECK(state_ == kBubbleShowingTimer || state_ == kBubbleHidingTimer);
+ DCHECK([NSThread isMainThread]);
+
+ if (state_ == kBubbleShowingTimer) {
+ SetState(kBubbleShowingFadeIn);
+ Fade(true);
+ } else {
+ SetState(kBubbleHidingFadeOut);
+ Fade(false);
+ }
+}
+
+void StatusBubbleMac::StartShowing() {
+ // Note that |SetState()| will |Attach()| or |Detach()| as required.
+
+ if (state_ == kBubbleHidden) {
+ // Arrange to begin fading in after a delay.
+ SetState(kBubbleShowingTimer);
+ StartTimer(kShowDelayMilliseconds);
+ } else if (state_ == kBubbleHidingFadeOut) {
+ // Cancel the fade-out in progress and replace it with a fade in.
+ SetState(kBubbleShowingFadeIn);
+ Fade(true);
+ } else if (state_ == kBubbleHidingTimer) {
+ // The bubble was already shown but was waiting to begin fading out. It's
+ // given a stay of execution.
+ SetState(kBubbleShown);
+ CancelTimer();
+ } else if (state_ == kBubbleShowingTimer) {
+ // The timer was already running but nothing was showing yet. Reaching
+ // this point means that there is a new request to show something. Start
+ // over again by resetting the timer, effectively invalidating the earlier
+ // request.
+ StartTimer(kShowDelayMilliseconds);
+ }
+
+ // If the state is kBubbleShown or kBubbleShowingFadeIn, leave everything
+ // alone.
+}
+
+void StatusBubbleMac::StartHiding() {
+ if (state_ == kBubbleShown) {
+ // Arrange to begin fading out after a delay.
+ SetState(kBubbleHidingTimer);
+ StartTimer(kHideDelayMilliseconds);
+ } else if (state_ == kBubbleShowingFadeIn) {
+ // Cancel the fade-in in progress and replace it with a fade out.
+ SetState(kBubbleHidingFadeOut);
+ Fade(false);
+ } else if (state_ == kBubbleShowingTimer) {
+ // The bubble was already hidden but was waiting to begin fading in. Too
+ // bad, it won't get the opportunity now.
+ SetState(kBubbleHidden);
+ CancelTimer();
+ }
+
+ // If the state is kBubbleHidden, kBubbleHidingFadeOut, or
+ // kBubbleHidingTimer, leave everything alone. The timer is not reset as
+ // with kBubbleShowingTimer in StartShowing() because a subsequent request
+ // to hide something while one is already in flight does not invalidate the
+ // earlier request.
+}
+
+void StatusBubbleMac::CancelExpandTimer() {
+ DCHECK([NSThread isMainThread]);
+ expand_timer_factory_.RevokeAll();
+}
+
+void StatusBubbleMac::ExpandBubble() {
+ // Calculate the width available for expanded and standard bubbles.
+ NSRect window_frame = CalculateWindowFrame(/*expand=*/true);
+ CGFloat max_bubble_width = NSWidth(window_frame);
+ CGFloat standard_bubble_width =
+ NSWidth(CalculateWindowFrame(/*expand=*/false));
+
+ // Generate the URL string that fits in the expanded bubble.
+ NSFont* font = [[window_ contentView] font];
+ gfx::Font font_chr(base::SysNSStringToWide([font fontName]),
+ [font pointSize]);
+ string16 expanded_url = gfx::ElideUrl(url_, font_chr,
+ max_bubble_width, UTF16ToWideHack(languages_));
+
+ // Scale width from gfx::Font in view coordinates to window coordinates.
+ int required_width_for_string =
+ font_chr.GetStringWidth(UTF16ToWide(expanded_url)) +
+ kTextPadding * 2 + kBubbleViewTextPositionX;
+ NSSize scaled_width = NSMakeSize(required_width_for_string, 0);
+ scaled_width = [[parent_ contentView] convertSize:scaled_width toView:nil];
+ required_width_for_string = scaled_width.width;
+
+ // The expanded width must be at least as wide as the standard width, but no
+ // wider than the maximum width for its parent frame.
+ int expanded_bubble_width =
+ std::max(standard_bubble_width,
+ std::min(max_bubble_width,
+ static_cast<CGFloat>(required_width_for_string)));
+
+ SetText(expanded_url, true);
+ is_expanded_ = true;
+ window_frame.size.width = expanded_bubble_width;
+
+ // In testing, don't do any animation.
+ if (immediate_) {
+ [window_ setFrame:window_frame display:YES];
+ return;
+ }
+
+ NSRect actual_window_frame = [window_ frame];
+ // Adjust status bubble origin if bubble was moved to the right.
+ // TODO(alekseys): fix for RTL.
+ if (NSMinX(actual_window_frame) > NSMinX(window_frame)) {
+ actual_window_frame.origin.x =
+ NSMaxX(actual_window_frame) - NSWidth(window_frame);
+ }
+ actual_window_frame.size.width = NSWidth(window_frame);
+
+ // Do not expand if it's going to cover mouse location.
+ if (NSPointInRect([NSEvent mouseLocation], actual_window_frame))
+ return;
+
+ [NSAnimationContext beginGrouping];
+ [[NSAnimationContext currentContext] setDuration:kExpansionDuration];
+ [[window_ animator] setFrame:actual_window_frame display:YES];
+ [NSAnimationContext endGrouping];
+}
+
+void StatusBubbleMac::UpdateSizeAndPosition() {
+ if (!window_)
+ return;
+
+ [window_ setFrame:CalculateWindowFrame(/*expand=*/false) display:YES];
+}
+
+void StatusBubbleMac::SwitchParentWindow(NSWindow* parent) {
+ DCHECK(parent);
+
+ // If not attached, just update our member variable and position.
+ if (!is_attached()) {
+ parent_ = parent;
+ [[window_ contentView] setThemeProvider:parent];
+ UpdateSizeAndPosition();
+ return;
+ }
+
+ Detach();
+ parent_ = parent;
+ [[window_ contentView] setThemeProvider:parent];
+ Attach();
+ UpdateSizeAndPosition();
+}
+
+NSRect StatusBubbleMac::CalculateWindowFrame(bool expanded_width) {
+ DCHECK(parent_);
+
+ NSRect screenRect;
+ if ([delegate_ respondsToSelector:@selector(statusBubbleBaseFrame)]) {
+ screenRect = [delegate_ statusBubbleBaseFrame];
+ screenRect.origin = [parent_ convertBaseToScreen:screenRect.origin];
+ } else {
+ screenRect = [parent_ frame];
+ }
+
+ NSSize size = NSMakeSize(0, kWindowHeight);
+ size = [[parent_ contentView] convertSize:size toView:nil];
+
+ if (expanded_width) {
+ size.width = screenRect.size.width;
+ } else {
+ size.width = kWindowWidthPercent * screenRect.size.width;
+ }
+
+ screenRect.size = size;
+ return screenRect;
+}
diff --git a/chrome/browser/ui/cocoa/status_bubble_mac_unittest.mm b/chrome/browser/ui/cocoa/status_bubble_mac_unittest.mm
new file mode 100644
index 0000000..5aa895a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/status_bubble_mac_unittest.mm
@@ -0,0 +1,584 @@
+// 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 <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "base/utf_string_conversions.h"
+#import "chrome/browser/ui/cocoa/bubble_view.h"
+#import "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/status_bubble_mac.h"
+#include "googleurl/src/gurl.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+// The test delegate records all of the status bubble object's state
+// transitions.
+@interface StatusBubbleMacTestDelegate : NSObject {
+ @private
+ NSWindow* window_; // Weak.
+ NSPoint baseFrameOffset_;
+ std::vector<StatusBubbleMac::StatusBubbleState> states_;
+}
+- (id)initWithWindow:(NSWindow*)window;
+- (void)forceBaseFrameOffset:(NSPoint)baseFrameOffset;
+- (NSRect)statusBubbleBaseFrame;
+- (void)statusBubbleWillEnterState:(StatusBubbleMac::StatusBubbleState)state;
+@end
+@implementation StatusBubbleMacTestDelegate
+- (id)initWithWindow:(NSWindow*)window {
+ if ((self = [super init])) {
+ window_ = window;
+ baseFrameOffset_ = NSMakePoint(0, 0);
+ }
+ return self;
+}
+- (void)forceBaseFrameOffset:(NSPoint)baseFrameOffset {
+ baseFrameOffset_ = baseFrameOffset;
+}
+- (NSRect)statusBubbleBaseFrame {
+ NSView* contentView = [window_ contentView];
+ NSRect baseFrame = [contentView convertRect:[contentView frame] toView:nil];
+ if (baseFrameOffset_.x > 0 || baseFrameOffset_.y > 0) {
+ baseFrame = NSOffsetRect(baseFrame, baseFrameOffset_.x, baseFrameOffset_.y);
+ baseFrame.size.width -= baseFrameOffset_.x;
+ baseFrame.size.height -= baseFrameOffset_.y;
+ }
+ return baseFrame;
+}
+- (void)statusBubbleWillEnterState:(StatusBubbleMac::StatusBubbleState)state {
+ states_.push_back(state);
+}
+- (std::vector<StatusBubbleMac::StatusBubbleState>*)states {
+ return &states_;
+}
+@end
+
+// This class implements, for testing purposes, a subclass of |StatusBubbleMac|
+// whose |MouseMoved()| method does nothing. (Ideally, we'd have a way of
+// controlling the "mouse" location, but the current implementation of
+// |StatusBubbleMac| uses |[NSEvent mouseLocation]| directly.) Without this,
+// tests can be flaky since results may depend on the mouse location.
+class StatusBubbleMacIgnoreMouseMoved : public StatusBubbleMac {
+ public:
+ StatusBubbleMacIgnoreMouseMoved(NSWindow* parent, id delegate)
+ : StatusBubbleMac(parent, delegate) {}
+
+ virtual void MouseMoved(const gfx::Point& location, bool left_content) {}
+};
+
+class StatusBubbleMacTest : public CocoaTest {
+ public:
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ NSWindow* window = test_window();
+ EXPECT_TRUE(window);
+ delegate_.reset(
+ [[StatusBubbleMacTestDelegate alloc] initWithWindow: window]);
+ EXPECT_TRUE(delegate_.get());
+ bubble_ = new StatusBubbleMacIgnoreMouseMoved(window, delegate_);
+ EXPECT_TRUE(bubble_);
+
+ // Turn off delays and transitions for test mode. This doesn't just speed
+ // things along, it's actually required to get StatusBubbleMac to behave
+ // synchronously, because the tests here don't know how to wait for
+ // results. This allows these tests to be much more complete with a
+ // minimal loss of coverage and without any heinous rearchitecting.
+ bubble_->immediate_ = true;
+
+ EXPECT_FALSE(bubble_->window_); // lazily creates window
+ }
+
+ virtual void TearDown() {
+ // Not using a scoped_ptr because bubble must be deleted before calling
+ // TearDown to get rid of bubble's window.
+ delete bubble_;
+ CocoaTest::TearDown();
+ }
+
+ bool IsVisible() {
+ if (![bubble_->window_ isVisible])
+ return false;
+ return [bubble_->window_ alphaValue] > 0.0;
+ }
+ NSString* GetText() {
+ return bubble_->status_text_;
+ }
+ NSString* GetURLText() {
+ return bubble_->url_text_;
+ }
+ NSString* GetBubbleViewText() {
+ BubbleView* bubbleView = [bubble_->window_ contentView];
+ return [bubbleView content];
+ }
+ NSWindow* GetWindow() {
+ return bubble_->window_;
+ }
+ NSWindow* GetParent() {
+ return bubble_->parent_;
+ }
+ StatusBubbleMac::StatusBubbleState GetState() {
+ return bubble_->state_;
+ }
+ void SetState(StatusBubbleMac::StatusBubbleState state) {
+ bubble_->SetState(state);
+ }
+ std::vector<StatusBubbleMac::StatusBubbleState>* States() {
+ return [delegate_ states];
+ }
+ StatusBubbleMac::StatusBubbleState StateAt(int index) {
+ return (*States())[index];
+ }
+ BrowserTestHelper browser_helper_;
+ scoped_nsobject<StatusBubbleMacTestDelegate> delegate_;
+ StatusBubbleMac* bubble_; // Strong.
+};
+
+TEST_F(StatusBubbleMacTest, SetStatus) {
+ bubble_->SetStatus(string16());
+ bubble_->SetStatus(UTF8ToUTF16("This is a test"));
+ EXPECT_NSEQ(@"This is a test", GetText());
+ EXPECT_TRUE(IsVisible());
+
+ // Set the status to the exact same thing again
+ bubble_->SetStatus(UTF8ToUTF16("This is a test"));
+ EXPECT_NSEQ(@"This is a test", GetText());
+
+ // Hide it
+ bubble_->SetStatus(string16());
+ EXPECT_FALSE(IsVisible());
+}
+
+TEST_F(StatusBubbleMacTest, SetURL) {
+ bubble_->SetURL(GURL(), string16());
+ EXPECT_FALSE(IsVisible());
+ bubble_->SetURL(GURL("bad url"), string16());
+ EXPECT_FALSE(IsVisible());
+ bubble_->SetURL(GURL("http://"), string16());
+ EXPECT_TRUE(IsVisible());
+ EXPECT_NSEQ(@"http:", GetURLText());
+ bubble_->SetURL(GURL("about:blank"), string16());
+ EXPECT_TRUE(IsVisible());
+ EXPECT_NSEQ(@"about:blank", GetURLText());
+ bubble_->SetURL(GURL("foopy://"), string16());
+ EXPECT_TRUE(IsVisible());
+ EXPECT_NSEQ(@"foopy://", GetURLText());
+ bubble_->SetURL(GURL("http://www.cnn.com"), string16());
+ EXPECT_TRUE(IsVisible());
+ EXPECT_NSEQ(@"www.cnn.com", GetURLText());
+}
+
+// Test hiding bubble that's already hidden.
+TEST_F(StatusBubbleMacTest, Hides) {
+ bubble_->SetStatus(UTF8ToUTF16("Showing"));
+ EXPECT_TRUE(IsVisible());
+ bubble_->Hide();
+ EXPECT_FALSE(IsVisible());
+ bubble_->Hide();
+ EXPECT_FALSE(IsVisible());
+}
+
+// Test the "main"/"backup" behavior in StatusBubbleMac::SetText().
+TEST_F(StatusBubbleMacTest, SetStatusAndURL) {
+ EXPECT_FALSE(IsVisible());
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ EXPECT_TRUE(IsVisible());
+ EXPECT_NSEQ(@"Status", GetBubbleViewText());
+ bubble_->SetURL(GURL("http://www.nytimes.com"), string16());
+ EXPECT_TRUE(IsVisible());
+ EXPECT_NSEQ(@"www.nytimes.com", GetBubbleViewText());
+ bubble_->SetURL(GURL(), string16());
+ EXPECT_TRUE(IsVisible());
+ EXPECT_NSEQ(@"Status", GetBubbleViewText());
+ bubble_->SetStatus(string16());
+ EXPECT_FALSE(IsVisible());
+ bubble_->SetURL(GURL("http://www.nytimes.com"), string16());
+ EXPECT_TRUE(IsVisible());
+ EXPECT_NSEQ(@"www.nytimes.com", GetBubbleViewText());
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ EXPECT_TRUE(IsVisible());
+ EXPECT_NSEQ(@"Status", GetBubbleViewText());
+ bubble_->SetStatus(string16());
+ EXPECT_TRUE(IsVisible());
+ EXPECT_NSEQ(@"www.nytimes.com", GetBubbleViewText());
+ bubble_->SetURL(GURL(), string16());
+ EXPECT_FALSE(IsVisible());
+}
+
+// Test that the status bubble goes through the correct delay and fade states.
+// The delay and fade duration are simulated and not actually experienced
+// during the test because StatusBubbleMacTest sets immediate_ mode.
+TEST_F(StatusBubbleMacTest, StateTransitions) {
+ // First, some sanity
+
+ EXPECT_FALSE(IsVisible());
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState());
+
+ States()->clear();
+ EXPECT_TRUE(States()->empty());
+
+ bubble_->SetStatus(string16());
+ EXPECT_FALSE(IsVisible());
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState());
+ EXPECT_TRUE(States()->empty()); // no change from initial kBubbleHidden state
+
+ // Next, a few ordinary cases
+
+ // Test StartShowing from kBubbleHidden
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ EXPECT_TRUE(IsVisible());
+ // Check GetState before checking States to make sure that all state
+ // transitions have been flushed to States.
+ EXPECT_EQ(StatusBubbleMac::kBubbleShown, GetState());
+ EXPECT_EQ(3u, States()->size());
+ EXPECT_EQ(StatusBubbleMac::kBubbleShowingTimer, StateAt(0));
+ EXPECT_EQ(StatusBubbleMac::kBubbleShowingFadeIn, StateAt(1));
+ EXPECT_EQ(StatusBubbleMac::kBubbleShown, StateAt(2));
+
+ // Test StartShowing from kBubbleShown with the same message
+ States()->clear();
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ EXPECT_TRUE(IsVisible());
+ EXPECT_EQ(StatusBubbleMac::kBubbleShown, GetState());
+ EXPECT_TRUE(States()->empty());
+
+ // Test StartShowing from kBubbleShown with a different message
+ bubble_->SetStatus(UTF8ToUTF16("New Status"));
+ EXPECT_TRUE(IsVisible());
+ EXPECT_EQ(StatusBubbleMac::kBubbleShown, GetState());
+ EXPECT_TRUE(States()->empty());
+
+ // Test StartHiding from kBubbleShown
+ bubble_->SetStatus(string16());
+ EXPECT_FALSE(IsVisible());
+ // Check GetState before checking States to make sure that all state
+ // transitions have been flushed to States.
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState());
+ EXPECT_EQ(3u, States()->size());
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidingTimer, StateAt(0));
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidingFadeOut, StateAt(1));
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, StateAt(2));
+
+ // Test StartHiding from kBubbleHidden
+ States()->clear();
+ bubble_->SetStatus(string16());
+ EXPECT_FALSE(IsVisible());
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState());
+ EXPECT_TRUE(States()->empty());
+
+ // Now, the edge cases
+
+ // Test StartShowing from kBubbleShowingTimer
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ SetState(StatusBubbleMac::kBubbleShowingTimer);
+ [GetWindow() setAlphaValue:0.0];
+ States()->clear();
+ EXPECT_TRUE(States()->empty());
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ EXPECT_EQ(StatusBubbleMac::kBubbleShown, GetState());
+ EXPECT_EQ(2u, States()->size());
+ EXPECT_EQ(StatusBubbleMac::kBubbleShowingFadeIn, StateAt(0));
+ EXPECT_EQ(StatusBubbleMac::kBubbleShown, StateAt(1));
+
+ // Test StartShowing from kBubbleShowingFadeIn
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ SetState(StatusBubbleMac::kBubbleShowingFadeIn);
+ [GetWindow() setAlphaValue:0.5];
+ States()->clear();
+ EXPECT_TRUE(States()->empty());
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ // The actual state values can't be tested in immediate_ mode because
+ // the window wasn't actually fading in. Without immediate_ mode,
+ // expect kBubbleShown.
+ bubble_->SetStatus(string16()); // Go back to a deterministic state.
+
+ // Test StartShowing from kBubbleHidingTimer
+ bubble_->SetStatus(string16());
+ SetState(StatusBubbleMac::kBubbleHidingTimer);
+ [GetWindow() setAlphaValue:1.0];
+ States()->clear();
+ EXPECT_TRUE(States()->empty());
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ EXPECT_EQ(StatusBubbleMac::kBubbleShown, GetState());
+ EXPECT_EQ(1u, States()->size());
+ EXPECT_EQ(StatusBubbleMac::kBubbleShown, StateAt(0));
+
+ // Test StartShowing from kBubbleHidingFadeOut
+ bubble_->SetStatus(string16());
+ SetState(StatusBubbleMac::kBubbleHidingFadeOut);
+ [GetWindow() setAlphaValue:0.5];
+ States()->clear();
+ EXPECT_TRUE(States()->empty());
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ EXPECT_EQ(StatusBubbleMac::kBubbleShown, GetState());
+ EXPECT_EQ(2u, States()->size());
+ EXPECT_EQ(StatusBubbleMac::kBubbleShowingFadeIn, StateAt(0));
+ EXPECT_EQ(StatusBubbleMac::kBubbleShown, StateAt(1));
+
+ // Test StartHiding from kBubbleShowingTimer
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ SetState(StatusBubbleMac::kBubbleShowingTimer);
+ [GetWindow() setAlphaValue:0.0];
+ States()->clear();
+ EXPECT_TRUE(States()->empty());
+ bubble_->SetStatus(string16());
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState());
+ EXPECT_EQ(1u, States()->size());
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, StateAt(0));
+
+ // Test StartHiding from kBubbleShowingFadeIn
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ SetState(StatusBubbleMac::kBubbleShowingFadeIn);
+ [GetWindow() setAlphaValue:0.5];
+ States()->clear();
+ EXPECT_TRUE(States()->empty());
+ bubble_->SetStatus(string16());
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState());
+ EXPECT_EQ(2u, States()->size());
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidingFadeOut, StateAt(0));
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, StateAt(1));
+
+ // Test StartHiding from kBubbleHidingTimer
+ bubble_->SetStatus(string16());
+ SetState(StatusBubbleMac::kBubbleHidingTimer);
+ [GetWindow() setAlphaValue:1.0];
+ States()->clear();
+ EXPECT_TRUE(States()->empty());
+ bubble_->SetStatus(string16());
+ // The actual state values can't be tested in immediate_ mode because
+ // the timer wasn't actually running. Without immediate_ mode, expect
+ // kBubbleHidingFadeOut and kBubbleHidden.
+ // Go back to a deterministic state.
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+
+ // Test StartHiding from kBubbleHidingFadeOut
+ bubble_->SetStatus(string16());
+ SetState(StatusBubbleMac::kBubbleHidingFadeOut);
+ [GetWindow() setAlphaValue:0.5];
+ States()->clear();
+ EXPECT_TRUE(States()->empty());
+ bubble_->SetStatus(string16());
+ // The actual state values can't be tested in immediate_ mode because
+ // the window wasn't actually fading out. Without immediate_ mode, expect
+ // kBubbleHidden.
+ // Go back to a deterministic state.
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+
+ // Test Hide from kBubbleHidden
+ bubble_->SetStatus(string16());
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState());
+ States()->clear();
+ EXPECT_TRUE(States()->empty());
+ bubble_->Hide();
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState());
+ EXPECT_TRUE(States()->empty());
+
+ // Test Hide from kBubbleShowingTimer
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ SetState(StatusBubbleMac::kBubbleShowingTimer);
+ [GetWindow() setAlphaValue:0.0];
+ States()->clear();
+ EXPECT_TRUE(States()->empty());
+ bubble_->Hide();
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState());
+ EXPECT_EQ(1u, States()->size());
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, StateAt(0));
+
+ // Test Hide from kBubbleShowingFadeIn
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ SetState(StatusBubbleMac::kBubbleShowingFadeIn);
+ [GetWindow() setAlphaValue:0.5];
+ States()->clear();
+ EXPECT_TRUE(States()->empty());
+ bubble_->Hide();
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState());
+ EXPECT_EQ(2u, States()->size());
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidingFadeOut, StateAt(0));
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, StateAt(1));
+
+ // Test Hide from kBubbleShown
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ States()->clear();
+ EXPECT_TRUE(States()->empty());
+ bubble_->Hide();
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState());
+ EXPECT_EQ(1u, States()->size());
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, StateAt(0));
+
+ // Test Hide from kBubbleHidingTimer
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ SetState(StatusBubbleMac::kBubbleHidingTimer);
+ States()->clear();
+ EXPECT_TRUE(States()->empty());
+ bubble_->Hide();
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState());
+ EXPECT_EQ(1u, States()->size());
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, StateAt(0));
+
+ // Test Hide from kBubbleHidingFadeOut
+ bubble_->SetStatus(UTF8ToUTF16("Status"));
+ SetState(StatusBubbleMac::kBubbleHidingFadeOut);
+ [GetWindow() setAlphaValue:0.5];
+ States()->clear();
+ EXPECT_TRUE(States()->empty());
+ bubble_->Hide();
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState());
+ EXPECT_EQ(1u, States()->size());
+ EXPECT_EQ(StatusBubbleMac::kBubbleHidden, StateAt(0));
+}
+
+TEST_F(StatusBubbleMacTest, Delete) {
+ NSWindow* window = test_window();
+ // Create and delete immediately.
+ StatusBubbleMac* bubble = new StatusBubbleMac(window, nil);
+ delete bubble;
+
+ // Create then delete while visible.
+ bubble = new StatusBubbleMac(window, nil);
+ bubble->SetStatus(UTF8ToUTF16("showing"));
+ delete bubble;
+}
+
+TEST_F(StatusBubbleMacTest, UpdateSizeAndPosition) {
+ // Test |UpdateSizeAndPosition()| when status bubble does not exist (shouldn't
+ // crash; shouldn't create window).
+ EXPECT_FALSE(GetWindow());
+ bubble_->UpdateSizeAndPosition();
+ EXPECT_FALSE(GetWindow());
+
+ // Create a status bubble (with contents) and call resize (without actually
+ // resizing); the frame size shouldn't change.
+ bubble_->SetStatus(UTF8ToUTF16("UpdateSizeAndPosition test"));
+ ASSERT_TRUE(GetWindow());
+ NSRect rect_before = [GetWindow() frame];
+ bubble_->UpdateSizeAndPosition();
+ NSRect rect_after = [GetWindow() frame];
+ EXPECT_TRUE(NSEqualRects(rect_before, rect_after));
+
+ // Move the window and call resize; only the origin should change.
+ NSWindow* window = test_window();
+ ASSERT_TRUE(window);
+ NSRect frame = [window frame];
+ rect_before = [GetWindow() frame];
+ frame.origin.x += 10.0; // (fairly arbitrary nonzero value)
+ frame.origin.y += 10.0; // (fairly arbitrary nonzero value)
+ [window setFrame:frame display:YES];
+ bubble_->UpdateSizeAndPosition();
+ rect_after = [GetWindow() frame];
+ EXPECT_NE(rect_before.origin.x, rect_after.origin.x);
+ EXPECT_NE(rect_before.origin.y, rect_after.origin.y);
+ EXPECT_EQ(rect_before.size.width, rect_after.size.width);
+ EXPECT_EQ(rect_before.size.height, rect_after.size.height);
+
+ // Resize the window (without moving). The origin shouldn't change. The width
+ // should change (in the current implementation), but not the height.
+ frame = [window frame];
+ rect_before = [GetWindow() frame];
+ frame.size.width += 50.0; // (fairly arbitrary nonzero value)
+ frame.size.height += 50.0; // (fairly arbitrary nonzero value)
+ [window setFrame:frame display:YES];
+ bubble_->UpdateSizeAndPosition();
+ rect_after = [GetWindow() frame];
+ EXPECT_EQ(rect_before.origin.x, rect_after.origin.x);
+ EXPECT_EQ(rect_before.origin.y, rect_after.origin.y);
+ EXPECT_NE(rect_before.size.width, rect_after.size.width);
+ EXPECT_EQ(rect_before.size.height, rect_after.size.height);
+}
+
+TEST_F(StatusBubbleMacTest, MovingWindowUpdatesPosition) {
+ NSWindow* window = test_window();
+
+ // Show the bubble and make sure it has the same origin as |window|.
+ bubble_->SetStatus(UTF8ToUTF16("Showing"));
+ NSWindow* child = GetWindow();
+ EXPECT_TRUE(NSEqualPoints([window frame].origin, [child frame].origin));
+
+ // Hide the bubble, move the window, and show it again.
+ bubble_->Hide();
+ NSRect frame = [window frame];
+ frame.origin.x += 50;
+ [window setFrame:frame display:YES];
+ bubble_->SetStatus(UTF8ToUTF16("Reshowing"));
+
+ // The bubble should reattach in the correct location.
+ child = GetWindow();
+ EXPECT_TRUE(NSEqualPoints([window frame].origin, [child frame].origin));
+}
+
+TEST_F(StatusBubbleMacTest, StatuBubbleRespectsBaseFrameLimits) {
+ NSWindow* window = test_window();
+
+ // Show the bubble and make sure it has the same origin as |window|.
+ bubble_->SetStatus(UTF8ToUTF16("Showing"));
+ NSWindow* child = GetWindow();
+ EXPECT_TRUE(NSEqualPoints([window frame].origin, [child frame].origin));
+
+ // Hide the bubble, change base frame offset, and show it again.
+ bubble_->Hide();
+
+ NSPoint baseFrameOffset = NSMakePoint(0, [window frame].size.height / 3);
+ EXPECT_GT(baseFrameOffset.y, 0);
+ [delegate_ forceBaseFrameOffset:baseFrameOffset];
+
+ bubble_->SetStatus(UTF8ToUTF16("Reshowing"));
+
+ // The bubble should reattach in the correct location.
+ child = GetWindow();
+ NSPoint expectedOrigin = [window frame].origin;
+ expectedOrigin.x += baseFrameOffset.x;
+ expectedOrigin.y += baseFrameOffset.y;
+ EXPECT_TRUE(NSEqualPoints(expectedOrigin, [child frame].origin));
+}
+
+TEST_F(StatusBubbleMacTest, ExpandBubble) {
+ NSWindow* window = test_window();
+ ASSERT_TRUE(window);
+ NSRect window_frame = [window frame];
+ window_frame.size.width = 600.0;
+ [window setFrame:window_frame display:YES];
+
+ // Check basic expansion
+ bubble_->SetStatus(UTF8ToUTF16("Showing"));
+ EXPECT_TRUE(IsVisible());
+ bubble_->SetURL(GURL("http://www.battersbox.com/peter_paul_and_mary.html"),
+ string16());
+ EXPECT_TRUE([GetURLText() hasSuffix:@"\u2026"]);
+ bubble_->ExpandBubble();
+ EXPECT_TRUE(IsVisible());
+ EXPECT_NSEQ(@"www.battersbox.com/peter_paul_and_mary.html", GetURLText());
+ bubble_->Hide();
+
+ // Make sure bubble resets after hide.
+ bubble_->SetStatus(UTF8ToUTF16("Showing"));
+ bubble_->SetURL(GURL("http://www.snickersnee.com/pioneer_fishstix.html"),
+ string16());
+ EXPECT_TRUE([GetURLText() hasSuffix:@"\u2026"]);
+ // ...and that it expands again properly.
+ bubble_->ExpandBubble();
+ EXPECT_NSEQ(@"www.snickersnee.com/pioneer_fishstix.html", GetURLText());
+ // ...again, again!
+ bubble_->SetURL(GURL("http://www.battersbox.com/peter_paul_and_mary.html"),
+ string16());
+ bubble_->ExpandBubble();
+ EXPECT_NSEQ(@"www.battersbox.com/peter_paul_and_mary.html", GetURLText());
+ bubble_->Hide();
+
+ window_frame = [window frame];
+ window_frame.size.width = 300.0;
+ [window setFrame:window_frame display:YES];
+
+ // Very long URL's will be cut off even in the expanded state.
+ bubble_->SetStatus(UTF8ToUTF16("Showing"));
+ const char veryLongUrl[] =
+ "http://www.diewahrscheinlichlaengstepralinederwelt.com/duuuuplo.html";
+ bubble_->SetURL(GURL(veryLongUrl), string16());
+ EXPECT_TRUE([GetURLText() hasSuffix:@"\u2026"]);
+ bubble_->ExpandBubble();
+ EXPECT_TRUE([GetURLText() hasSuffix:@"\u2026"]);
+}
+
+
diff --git a/chrome/browser/ui/cocoa/status_icons/status_icon_mac.h b/chrome/browser/ui/cocoa/status_icons/status_icon_mac.h
new file mode 100644
index 0000000..c414ec90
--- /dev/null
+++ b/chrome/browser/ui/cocoa/status_icons/status_icon_mac.h
@@ -0,0 +1,44 @@
+// 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_UI_COCOA_STATUS_ICONS_STATUS_ICON_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_STATUS_ICONS_STATUS_ICON_MAC_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "base/string16.h"
+#include "chrome/browser/status_icons/status_icon.h"
+
+class SkBitmap;
+@class NSStatusItem;
+@class StatusItemController;
+
+class StatusIconMac : public StatusIcon {
+ public:
+ StatusIconMac();
+ virtual ~StatusIconMac();
+
+ // Overridden from StatusIcon
+ virtual void SetImage(const SkBitmap& image);
+ virtual void SetPressedImage(const SkBitmap& image);
+ virtual void SetToolTip(const string16& tool_tip);
+
+ protected:
+ // Overridden from StatusIcon.
+ virtual void UpdatePlatformContextMenu(menus::MenuModel* menu);
+
+ private:
+ // Getter for item_ that allows lazy initialization.
+ NSStatusItem* item();
+ scoped_nsobject<NSStatusItem> item_;
+
+ scoped_nsobject<StatusItemController> controller_;
+
+ DISALLOW_COPY_AND_ASSIGN(StatusIconMac);
+};
+
+
+#endif // CHROME_BROWSER_UI_COCOA_STATUS_ICONS_STATUS_ICON_MAC_H_
diff --git a/chrome/browser/ui/cocoa/status_icons/status_icon_mac.mm b/chrome/browser/ui/cocoa/status_icons/status_icon_mac.mm
new file mode 100644
index 0000000..3f3d5a1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/status_icons/status_icon_mac.mm
@@ -0,0 +1,82 @@
+// 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 "chrome/browser/ui/cocoa/status_icons/status_icon_mac.h"
+
+#include "base/logging.h"
+#include "base/sys_string_conversions.h"
+#include "skia/ext/skia_utils_mac.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+@interface StatusItemController : NSObject {
+ StatusIconMac* statusIcon_; // weak
+}
+- initWithIcon:(StatusIconMac*)icon;
+- (void)handleClick:(id)sender;
+
+@end // @interface StatusItemController
+
+@implementation StatusItemController
+
+- (id)initWithIcon:(StatusIconMac*)icon {
+ statusIcon_ = icon;
+ return self;
+}
+
+- (void)handleClick:(id)sender {
+ // Pass along the click notification to our owner.
+ DCHECK(statusIcon_);
+ statusIcon_->DispatchClickEvent();
+}
+
+@end
+
+StatusIconMac::StatusIconMac()
+ : item_(NULL) {
+ controller_.reset([[StatusItemController alloc] initWithIcon:this]);
+}
+
+StatusIconMac::~StatusIconMac() {
+ // Remove the status item from the status bar.
+ if (item_)
+ [[NSStatusBar systemStatusBar] removeStatusItem:item_];
+}
+
+NSStatusItem* StatusIconMac::item() {
+ if (!item_.get()) {
+ // Create a new status item.
+ item_.reset([[[NSStatusBar systemStatusBar]
+ statusItemWithLength:NSSquareStatusItemLength] retain]);
+ [item_ setEnabled:YES];
+ [item_ setTarget:controller_];
+ [item_ setAction:@selector(handleClick:)];
+ [item_ setHighlightMode:YES];
+ }
+ return item_.get();
+}
+
+void StatusIconMac::SetImage(const SkBitmap& bitmap) {
+ if (!bitmap.isNull()) {
+ NSImage* image = gfx::SkBitmapToNSImage(bitmap);
+ if (image)
+ [item() setImage:image];
+ }
+}
+
+void StatusIconMac::SetPressedImage(const SkBitmap& bitmap) {
+ if (!bitmap.isNull()) {
+ NSImage* image = gfx::SkBitmapToNSImage(bitmap);
+ if (image)
+ [item() setAlternateImage:image];
+ }
+}
+
+void StatusIconMac::SetToolTip(const string16& tool_tip) {
+ [item() setToolTip:base::SysUTF16ToNSString(tool_tip)];
+}
+
+void StatusIconMac::UpdatePlatformContextMenu(menus::MenuModel* menu) {
+ // TODO(atwilson): Add support for context menus for Mac when actually needed
+ // (not yet used by anything) - http://crbug.com/37375.
+}
diff --git a/chrome/browser/ui/cocoa/status_icons/status_icon_mac_unittest.mm b/chrome/browser/ui/cocoa/status_icons/status_icon_mac_unittest.mm
new file mode 100644
index 0000000..45d1950
--- /dev/null
+++ b/chrome/browser/ui/cocoa/status_icons/status_icon_mac_unittest.mm
@@ -0,0 +1,30 @@
+// 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 "app/resource_bundle.h"
+#include "base/string_util.h"
+#include "base/utf_string_conversions.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/status_icons/status_icon_mac.h"
+#include "grit/browser_resources.h"
+#include "grit/theme_resources.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+class SkBitmap;
+
+class StatusIconMacTest : public CocoaTest {
+};
+
+TEST_F(StatusIconMacTest, Create) {
+ // Create an icon, set the tool tip, then shut it down (checks for leaks).
+ scoped_ptr<StatusIcon> icon(new StatusIconMac());
+ SkBitmap* bitmap = ResourceBundle::GetSharedInstance().GetBitmapNamed(
+ IDR_STATUS_TRAY_ICON);
+ icon->SetImage(*bitmap);
+ SkBitmap* pressed = ResourceBundle::GetSharedInstance().GetBitmapNamed(
+ IDR_STATUS_TRAY_ICON_PRESSED);
+ icon->SetPressedImage(*pressed);
+ icon->SetToolTip(ASCIIToUTF16("tool tip"));
+}
diff --git a/chrome/browser/ui/cocoa/status_icons/status_tray_mac.h b/chrome/browser/ui/cocoa/status_icons/status_tray_mac.h
new file mode 100644
index 0000000..0b8326f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/status_icons/status_tray_mac.h
@@ -0,0 +1,24 @@
+// 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_UI_COCOA_STATUS_ICONS_STATUS_TRAY_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_STATUS_ICONS_STATUS_TRAY_MAC_H_
+#pragma once
+
+#include "chrome/browser/status_icons/status_tray.h"
+
+class StatusTrayMac : public StatusTray {
+ public:
+ StatusTrayMac();
+
+ protected:
+ // Factory method for creating a status icon.
+ virtual StatusIcon* CreatePlatformStatusIcon();
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(StatusTrayMac);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_STATUS_ICONS_STATUS_TRAY_MAC_H_
+
diff --git a/chrome/browser/ui/cocoa/status_icons/status_tray_mac.mm b/chrome/browser/ui/cocoa/status_icons/status_tray_mac.mm
new file mode 100644
index 0000000..5d6c3e2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/status_icons/status_tray_mac.mm
@@ -0,0 +1,18 @@
+// 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 "chrome/browser/ui/cocoa/status_icons/status_tray_mac.h"
+
+#include "chrome/browser/ui/cocoa/status_icons/status_icon_mac.h"
+
+StatusTray* StatusTray::Create() {
+ return new StatusTrayMac();
+}
+
+StatusTrayMac::StatusTrayMac() {
+}
+
+StatusIcon* StatusTrayMac::CreatePlatformStatusIcon() {
+ return new StatusIconMac();
+}
diff --git a/chrome/browser/ui/cocoa/styled_text_field.h b/chrome/browser/ui/cocoa/styled_text_field.h
new file mode 100644
index 0000000..68a65b7
--- /dev/null
+++ b/chrome/browser/ui/cocoa/styled_text_field.h
@@ -0,0 +1,29 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+@class StyledTextFieldCell;
+
+// An implementation of NSTextField that is designed to work with
+// StyledTextFieldCell. Provides methods to redraw the field when cell
+// decorations have changed and overrides |mouseDown:| to properly handle clicks
+// in sections of the cell with decorations.
+@interface StyledTextField : NSTextField {
+}
+
+// Repositions and redraws the field editor. Call this method when the cell's
+// text frame has changed (whenever changing cell decorations).
+- (void)resetFieldEditorFrameIfNeeded;
+
+// Returns the amount of the field's width which is not being taken up
+// by the text contents. May be negative if the contents are large
+// enough to scroll.
+- (CGFloat)availableDecorationWidth;
+
+@end
+
+@interface StyledTextField (ExposedForTesting)
+- (StyledTextFieldCell*)styledTextFieldCell;
+@end
diff --git a/chrome/browser/ui/cocoa/styled_text_field.mm b/chrome/browser/ui/cocoa/styled_text_field.mm
new file mode 100644
index 0000000..31dd3a7
--- /dev/null
+++ b/chrome/browser/ui/cocoa/styled_text_field.mm
@@ -0,0 +1,61 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/styled_text_field.h"
+
+#include "base/logging.h"
+#import "chrome/browser/ui/cocoa/styled_text_field_cell.h"
+
+@implementation StyledTextField
+
+- (StyledTextFieldCell*)styledTextFieldCell {
+ DCHECK([[self cell] isKindOfClass:[StyledTextFieldCell class]]);
+ return static_cast<StyledTextFieldCell*>([self cell]);
+}
+
+// Cocoa text fields are edited by placing an NSTextView as subview,
+// positioned by the cell's -editWithFrame:inView:... method. Using
+// the standard -makeFirstResponder: machinery to reposition the field
+// editor results in resetting the field editor's editing state, which
+// AutocompleteEditViewMac monitors. This causes problems because
+// editing can require the field editor to be repositioned, which
+// could disrupt editing. This code repositions the subview directly,
+// which causes no editing-state changes.
+- (void)resetFieldEditorFrameIfNeeded {
+ // No action if not editing.
+ NSText* editor = [self currentEditor];
+ if (!editor) {
+ return;
+ }
+
+ // When editing, we should have exactly one subview, which is a
+ // clipview containing the editor (for purposes of scrolling).
+ NSArray* subviews = [self subviews];
+ DCHECK_EQ([subviews count], 1U);
+ DCHECK([editor isDescendantOf:self]);
+ if ([subviews count] == 0) {
+ return;
+ }
+
+ // If the frame is already right, don't make any visible changes.
+ StyledTextFieldCell* cell = [self styledTextFieldCell];
+ const NSRect frame([cell drawingRectForBounds:[self bounds]]);
+ NSView* subview = [subviews objectAtIndex:0];
+ if (NSEqualRects(frame, [subview frame])) {
+ return;
+ }
+
+ [subview setFrame:frame];
+
+ // Make sure the selection remains visible.
+ [editor scrollRangeToVisible:[editor selectedRange]];
+}
+
+- (CGFloat)availableDecorationWidth {
+ NSAttributedString* as = [self attributedStringValue];
+ const NSSize size([as size]);
+ const NSRect bounds([self bounds]);
+ return NSWidth(bounds) - size.width;
+}
+@end
diff --git a/chrome/browser/ui/cocoa/styled_text_field_cell.h b/chrome/browser/ui/cocoa/styled_text_field_cell.h
new file mode 100644
index 0000000..55fed3c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/styled_text_field_cell.h
@@ -0,0 +1,58 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_STYLED_TEXT_FIELD_CELL_H_
+#define CHROME_BROWSER_UI_COCOA_STYLED_TEXT_FIELD_CELL_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+typedef enum {
+ StyledTextFieldCellRoundedAll = 0,
+ StyledTextFieldCellRoundedLeft = 1
+} StyledTextFieldCellRoundedFlags;
+
+// StyledTextFieldCell customizes the look of the standard Cocoa text field.
+// The border and focus ring are modified, as is the font baseline. Subclasses
+// can override |drawInteriorWithFrame:inView:| to provide custom drawing for
+// decorations, but they must make sure to call the superclass' implementation
+// with a modified frame after performing any custom drawing.
+
+@interface StyledTextFieldCell : NSTextFieldCell {
+}
+
+@end
+
+// Methods intended to be overridden by subclasses, not part of the public API
+// and should not be called outside of subclasses.
+@interface StyledTextFieldCell (ProtectedMethods)
+
+// Return the portion of the cell to show the text cursor over. The default
+// implementation returns the full |cellFrame|. Subclasses should override this
+// method if they add any decorations.
+- (NSRect)textCursorFrameForFrame:(NSRect)cellFrame;
+
+// Return the portion of the cell to use for text display. This corresponds to
+// the frame with our added decorations sliced off. The default implementation
+// returns the full |cellFrame|, as by default there are no decorations.
+// Subclasses should override this method if they add any decorations.
+- (NSRect)textFrameForFrame:(NSRect)cellFrame;
+
+// Baseline adjust for the text in this cell. Defaults to 0. Subclasses should
+// override as needed.
+- (CGFloat)baselineAdjust;
+
+// Radius of the corners of the field. Defaults to square corners (0.0).
+- (CGFloat)cornerRadius;
+
+// Which corners of the field to round. Defaults to RoundedAll.
+- (StyledTextFieldCellRoundedFlags)roundedFlags;
+
+// Returns YES if a light themed bezel should be drawn under the text field.
+// Default implementation returns NO.
+- (BOOL)shouldDrawBezel;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_STYLED_TEXT_FIELD_CELL_H_
diff --git a/chrome/browser/ui/cocoa/styled_text_field_cell.mm b/chrome/browser/ui/cocoa/styled_text_field_cell.mm
new file mode 100644
index 0000000..56b0cf3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/styled_text_field_cell.mm
@@ -0,0 +1,217 @@
+// 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/ui/cocoa/styled_text_field_cell.h"
+
+#include "app/resource_bundle.h"
+#include "base/logging.h"
+#include "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#include "gfx/font.h"
+#include "grit/theme_resources.h"
+#import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h"
+
+namespace {
+
+NSBezierPath* RectPathWithInset(StyledTextFieldCellRoundedFlags roundedFlags,
+ const NSRect frame,
+ const CGFloat inset,
+ const CGFloat outerRadius) {
+ NSRect insetFrame = NSInsetRect(frame, inset, inset);
+
+ if (outerRadius > 0.0) {
+ CGFloat leftRadius = outerRadius - inset;
+ CGFloat rightRadius =
+ (roundedFlags == StyledTextFieldCellRoundedLeft) ? 0 : leftRadius;
+
+ return [NSBezierPath gtm_bezierPathWithRoundRect:insetFrame
+ topLeftCornerRadius:leftRadius
+ topRightCornerRadius:rightRadius
+ bottomLeftCornerRadius:leftRadius
+ bottomRightCornerRadius:rightRadius];
+ } else {
+ return [NSBezierPath bezierPathWithRect:insetFrame];
+ }
+}
+
+// Similar to |NSRectFill()|, additionally sets |color| as the fill
+// color. |outerRadius| greater than 0.0 uses rounded corners, with
+// inset backed out of the radius.
+void FillRectWithInset(StyledTextFieldCellRoundedFlags roundedFlags,
+ const NSRect frame,
+ const CGFloat inset,
+ const CGFloat outerRadius,
+ NSColor* color) {
+ NSBezierPath* path =
+ RectPathWithInset(roundedFlags, frame, inset, outerRadius);
+ [color setFill];
+ [path fill];
+}
+
+// Similar to |NSFrameRectWithWidth()|, additionally sets |color| as
+// the stroke color (as opposed to the fill color). |outerRadius|
+// greater than 0.0 uses rounded corners, with inset backed out of the
+// radius.
+void FrameRectWithInset(StyledTextFieldCellRoundedFlags roundedFlags,
+ const NSRect frame,
+ const CGFloat inset,
+ const CGFloat outerRadius,
+ const CGFloat lineWidth,
+ NSColor* color) {
+ const CGFloat finalInset = inset + (lineWidth / 2.0);
+ NSBezierPath* path =
+ RectPathWithInset(roundedFlags, frame, finalInset, outerRadius);
+ [color setStroke];
+ [path setLineWidth:lineWidth];
+ [path stroke];
+}
+
+// TODO(shess): Maybe we need a |cocoa_util.h|?
+class ScopedSaveGraphicsState {
+ public:
+ ScopedSaveGraphicsState()
+ : context_([NSGraphicsContext currentContext]) {
+ [context_ saveGraphicsState];
+ }
+ explicit ScopedSaveGraphicsState(NSGraphicsContext* context)
+ : context_(context) {
+ [context_ saveGraphicsState];
+ }
+ ~ScopedSaveGraphicsState() {
+ [context_ restoreGraphicsState];
+ }
+
+private:
+ NSGraphicsContext* context_;
+};
+
+} // namespace
+
+@implementation StyledTextFieldCell
+
+- (CGFloat)baselineAdjust {
+ return 0.0;
+}
+
+- (CGFloat)cornerRadius {
+ return 0.0;
+}
+
+- (StyledTextFieldCellRoundedFlags)roundedFlags {
+ return StyledTextFieldCellRoundedAll;
+}
+
+- (BOOL)shouldDrawBezel {
+ return NO;
+}
+
+// Returns the same value as textCursorFrameForFrame, but does not call it
+// directly to avoid potential infinite loops.
+- (NSRect)textFrameForFrame:(NSRect)cellFrame {
+ return NSInsetRect(cellFrame, 0, [self baselineAdjust]);
+}
+
+// Returns the same value as textFrameForFrame, but does not call it directly to
+// avoid potential infinite loops.
+- (NSRect)textCursorFrameForFrame:(NSRect)cellFrame {
+ return NSInsetRect(cellFrame, 0, [self baselineAdjust]);
+}
+
+// Override to show the I-beam cursor only in the area given by
+// |textCursorFrameForFrame:|.
+- (void)resetCursorRect:(NSRect)cellFrame inView:(NSView *)controlView {
+ [super resetCursorRect:[self textCursorFrameForFrame:cellFrame]
+ inView:controlView];
+}
+
+// For NSTextFieldCell this is the area within the borders. For our
+// purposes, we count the info decorations as being part of the
+// border.
+- (NSRect)drawingRectForBounds:(NSRect)theRect {
+ return [super drawingRectForBounds:[self textFrameForFrame:theRect]];
+}
+
+// TODO(shess): This code is manually drawing the cell's border area,
+// but otherwise the cell assumes -setBordered:YES for purposes of
+// calculating things like the editing area. This is probably
+// incorrect. I know that this affects -drawingRectForBounds:.
+- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
+ DCHECK([controlView isFlipped]);
+ StyledTextFieldCellRoundedFlags roundedFlags = [self roundedFlags];
+
+ // TODO(shess): This inset is also reflected by |kFieldVisualInset|
+ // in autocomplete_popup_view_mac.mm.
+ const NSRect frame = NSInsetRect(cellFrame, 0, 1);
+ const CGFloat radius = [self cornerRadius];
+
+ // Paint button background image if there is one (otherwise the border won't
+ // look right).
+ BrowserThemeProvider* themeProvider =
+ static_cast<BrowserThemeProvider*>([[controlView window] themeProvider]);
+ if (themeProvider) {
+ NSColor* backgroundImageColor =
+ themeProvider->GetNSImageColorNamed(IDR_THEME_BUTTON_BACKGROUND, false);
+ if (backgroundImageColor) {
+ // Set the phase to match window.
+ NSRect trueRect = [controlView convertRect:cellFrame toView:nil];
+ NSPoint midPoint = NSMakePoint(NSMinX(trueRect), NSMaxY(trueRect));
+ [[NSGraphicsContext currentContext] setPatternPhase:midPoint];
+
+ // NOTE(shess): This seems like it should be using a 0.0 inset,
+ // but AFAICT using a 0.5 inset is important in mixing the
+ // toolbar background and the omnibox background.
+ FillRectWithInset(roundedFlags, frame, 0.5, radius, backgroundImageColor);
+ }
+
+ // Draw the outer stroke (over the background).
+ BOOL active = [[controlView window] isMainWindow];
+ NSColor* strokeColor = themeProvider->GetNSColor(
+ active ? BrowserThemeProvider::COLOR_TOOLBAR_BUTTON_STROKE :
+ BrowserThemeProvider::COLOR_TOOLBAR_BUTTON_STROKE_INACTIVE,
+ true);
+ FrameRectWithInset(roundedFlags, frame, 0.0, radius, 1.0, strokeColor);
+ }
+
+ // Fill interior with background color.
+ FillRectWithInset(roundedFlags, frame, 1.0, radius, [self backgroundColor]);
+
+ // Draw the shadow. For the rounded-rect case, the shadow needs to
+ // slightly turn in at the corners. |shadowFrame| is at the same
+ // midline as the inner border line on the top and left, but at the
+ // outer border line on the bottom and right. The clipping change
+ // will clip the bottom and right edges (and corner).
+ {
+ ScopedSaveGraphicsState state;
+ [RectPathWithInset(roundedFlags, frame, 1.0, radius) addClip];
+ const NSRect shadowFrame = NSOffsetRect(frame, 0.5, 0.5);
+ NSColor* shadowShade = [NSColor colorWithCalibratedWhite:0.0 alpha:0.05];
+ FrameRectWithInset(roundedFlags, shadowFrame, 0.5, radius - 0.5,
+ 1.0, shadowShade);
+ }
+
+ // Draw optional bezel below bottom stroke.
+ if ([self shouldDrawBezel] && themeProvider &&
+ themeProvider->UsingDefaultTheme()) {
+
+ [themeProvider->GetNSColor(
+ BrowserThemeProvider::COLOR_TOOLBAR_BEZEL, true) set];
+ NSRect bezelRect = NSMakeRect(cellFrame.origin.x,
+ NSMaxY(cellFrame) - 0.5,
+ NSWidth(cellFrame),
+ 1.0);
+ bezelRect = NSInsetRect(bezelRect, radius - 0.5, 0.0);
+ NSRectFill(bezelRect);
+ }
+
+ // Draw the focus ring if needed.
+ if ([self showsFirstResponder]) {
+ NSColor* color =
+ [[NSColor keyboardFocusIndicatorColor] colorWithAlphaComponent:0.5];
+ FrameRectWithInset(roundedFlags, frame, 0.0, radius, 2.0, color);
+ }
+
+ [self drawInteriorWithFrame:cellFrame inView:controlView];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/styled_text_field_cell_unittest.mm b/chrome/browser/ui/cocoa/styled_text_field_cell_unittest.mm
new file mode 100644
index 0000000..2df3c65
--- /dev/null
+++ b/chrome/browser/ui/cocoa/styled_text_field_cell_unittest.mm
@@ -0,0 +1,93 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/styled_text_field_cell.h"
+#import "chrome/browser/ui/cocoa/styled_text_field_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+// Width of the field so that we don't have to ask |field_| for it all
+// the time.
+const CGFloat kWidth(300.0);
+
+class StyledTextFieldCellTest : public CocoaTest {
+ public:
+ StyledTextFieldCellTest() {
+ // Make sure this is wide enough to play games with the cell
+ // decorations.
+ const NSRect frame = NSMakeRect(0, 0, kWidth, 30);
+
+ scoped_nsobject<StyledTextFieldTestCell> cell(
+ [[StyledTextFieldTestCell alloc] initTextCell:@"Testing"]);
+ cell_ = cell.get();
+ [cell_ setEditable:YES];
+ [cell_ setBordered:YES];
+
+ scoped_nsobject<NSTextField> view(
+ [[NSTextField alloc] initWithFrame:frame]);
+ view_ = view.get();
+ [view_ setCell:cell_];
+
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ NSTextField* view_;
+ StyledTextFieldTestCell* cell_;
+};
+
+// Basic view tests (AddRemove, Display).
+TEST_VIEW(StyledTextFieldCellTest, view_);
+
+// Test drawing, mostly to ensure nothing leaks or crashes.
+TEST_F(StyledTextFieldCellTest, FocusedDisplay) {
+ [view_ display];
+
+ // Test focused drawing.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:view_];
+ [view_ display];
+ [test_window() clearPretendKeyWindowAndFirstResponder];
+
+ // Test display of various cell configurations.
+ [cell_ setLeftMargin:5];
+ [view_ display];
+
+ [cell_ setRightMargin:15];
+ [view_ display];
+}
+
+// The editor frame should be slightly inset from the text frame.
+TEST_F(StyledTextFieldCellTest, DrawingRectForBounds) {
+ const NSRect bounds = [view_ bounds];
+ NSRect textFrame = [cell_ textFrameForFrame:bounds];
+ NSRect drawingRect = [cell_ drawingRectForBounds:bounds];
+
+ EXPECT_FALSE(NSIsEmptyRect(drawingRect));
+ EXPECT_TRUE(NSContainsRect(textFrame, NSInsetRect(drawingRect, 1, 1)));
+
+ [cell_ setLeftMargin:10];
+ textFrame = [cell_ textFrameForFrame:bounds];
+ drawingRect = [cell_ drawingRectForBounds:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(drawingRect));
+ EXPECT_TRUE(NSContainsRect(textFrame, NSInsetRect(drawingRect, 1, 1)));
+
+ [cell_ setRightMargin:20];
+ textFrame = [cell_ textFrameForFrame:bounds];
+ drawingRect = [cell_ drawingRectForBounds:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(drawingRect));
+ EXPECT_TRUE(NSContainsRect(NSInsetRect(textFrame, 1, 1), drawingRect));
+
+ [cell_ setLeftMargin:0];
+ textFrame = [cell_ textFrameForFrame:bounds];
+ drawingRect = [cell_ drawingRectForBounds:bounds];
+ EXPECT_FALSE(NSIsEmptyRect(drawingRect));
+ EXPECT_TRUE(NSContainsRect(NSInsetRect(textFrame, 1, 1), drawingRect));
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/styled_text_field_test_helper.h b/chrome/browser/ui/cocoa/styled_text_field_test_helper.h
new file mode 100644
index 0000000..eb90cbf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/styled_text_field_test_helper.h
@@ -0,0 +1,16 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+#import "chrome/browser/ui/cocoa/styled_text_field_cell.h"
+
+// Subclass of StyledTextFieldCell that allows you to slice off sections on the
+// left and right of the cell.
+@interface StyledTextFieldTestCell : StyledTextFieldCell {
+ CGFloat leftMargin_;
+ CGFloat rightMargin_;
+}
+@property (nonatomic, assign) CGFloat leftMargin;
+@property (nonatomic, assign) CGFloat rightMargin;
+@end
diff --git a/chrome/browser/ui/cocoa/styled_text_field_test_helper.mm b/chrome/browser/ui/cocoa/styled_text_field_test_helper.mm
new file mode 100644
index 0000000..20f566d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/styled_text_field_test_helper.mm
@@ -0,0 +1,18 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+#import "chrome/browser/ui/cocoa/styled_text_field_test_helper.h"
+
+@implementation StyledTextFieldTestCell
+@synthesize leftMargin = leftMargin_;
+@synthesize rightMargin = rightMargin_;
+
+- (NSRect)textFrameForFrame:(NSRect)frame {
+ NSRect textFrame = [super textFrameForFrame:frame];
+ textFrame.origin.x += leftMargin_;
+ textFrame.size.width -= (leftMargin_ + rightMargin_);
+ return textFrame;
+}
+@end
diff --git a/chrome/browser/ui/cocoa/styled_text_field_unittest.mm b/chrome/browser/ui/cocoa/styled_text_field_unittest.mm
new file mode 100644
index 0000000..dfaaa5fa
--- /dev/null
+++ b/chrome/browser/ui/cocoa/styled_text_field_unittest.mm
@@ -0,0 +1,198 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/styled_text_field.h"
+#import "chrome/browser/ui/cocoa/styled_text_field_cell.h"
+#import "chrome/browser/ui/cocoa/styled_text_field_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+
+namespace {
+
+// Width of the field so that we don't have to ask |field_| for it all
+// the time.
+static const CGFloat kWidth(300.0);
+
+class StyledTextFieldTest : public CocoaTest {
+ public:
+ StyledTextFieldTest() {
+ // Make sure this is wide enough to play games with the cell
+ // decorations.
+ NSRect frame = NSMakeRect(0, 0, kWidth, 30);
+
+ scoped_nsobject<StyledTextFieldTestCell> cell(
+ [[StyledTextFieldTestCell alloc] initTextCell:@"Testing"]);
+ cell_ = cell.get();
+ [cell_ setEditable:YES];
+ [cell_ setBordered:YES];
+
+ scoped_nsobject<StyledTextField> field(
+ [[StyledTextField alloc] initWithFrame:frame]);
+ field_ = field.get();
+ [field_ setCell:cell_];
+
+ [[test_window() contentView] addSubview:field_];
+ }
+
+ // Helper to return the field-editor frame being used w/in |field_|.
+ NSRect EditorFrame() {
+ EXPECT_TRUE([field_ currentEditor]);
+ EXPECT_EQ([[field_ subviews] count], 1U);
+ if ([[field_ subviews] count] > 0) {
+ return [[[field_ subviews] objectAtIndex:0] frame];
+ } else {
+ // Return something which won't work so the caller can soldier
+ // on.
+ return NSZeroRect;
+ }
+ }
+
+ StyledTextField* field_;
+ StyledTextFieldTestCell* cell_;
+};
+
+// Basic view tests (AddRemove, Display).
+TEST_VIEW(StyledTextFieldTest, field_);
+
+// Test that we get the same cell from -cell and
+// -styledTextFieldCell.
+TEST_F(StyledTextFieldTest, Cell) {
+ StyledTextFieldCell* cell = [field_ styledTextFieldCell];
+ EXPECT_EQ(cell, [field_ cell]);
+ EXPECT_TRUE(cell != nil);
+}
+
+// Test that becoming first responder sets things up correctly.
+TEST_F(StyledTextFieldTest, FirstResponder) {
+ EXPECT_EQ(nil, [field_ currentEditor]);
+ EXPECT_EQ([[field_ subviews] count], 0U);
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ EXPECT_FALSE(nil == [field_ currentEditor]);
+ EXPECT_EQ([[field_ subviews] count], 1U);
+ EXPECT_TRUE([[field_ currentEditor] isDescendantOf:field_]);
+}
+
+TEST_F(StyledTextFieldTest, AvailableDecorationWidth) {
+ // A fudge factor to account for how much space the border takes up.
+ // The test shouldn't be too dependent on the field's internals, but
+ // it also shouldn't let deranged cases fall through the cracks
+ // (like nothing available with no text, or everything available
+ // with some text).
+ const CGFloat kBorderWidth = 20.0;
+
+ // With no contents, almost the entire width is available for
+ // decorations.
+ [field_ setStringValue:@""];
+ CGFloat availableWidth = [field_ availableDecorationWidth];
+ EXPECT_LE(availableWidth, kWidth);
+ EXPECT_GT(availableWidth, kWidth - kBorderWidth);
+
+ // With minor contents, most of the remaining width is available for
+ // decorations.
+ NSDictionary* attributes =
+ [NSDictionary dictionaryWithObject:[field_ font]
+ forKey:NSFontAttributeName];
+ NSString* string = @"Hello world";
+ const NSSize size([string sizeWithAttributes:attributes]);
+ [field_ setStringValue:string];
+ availableWidth = [field_ availableDecorationWidth];
+ EXPECT_LE(availableWidth, kWidth - size.width);
+ EXPECT_GT(availableWidth, kWidth - size.width - kBorderWidth);
+
+ // With huge contents, nothing at all is left for decorations.
+ string = @"A long string which is surely wider than field_ can hold.";
+ [field_ setStringValue:string];
+ availableWidth = [field_ availableDecorationWidth];
+ EXPECT_LT(availableWidth, 0.0);
+}
+
+// Test drawing, mostly to ensure nothing leaks or crashes.
+TEST_F(StyledTextFieldTest, Display) {
+ [field_ display];
+
+ // Test focused drawing.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ [field_ display];
+}
+
+// Test that the field editor gets the same bounds when focus is delivered by
+// the standard focusing machinery, or by -resetFieldEditorFrameIfNeeded.
+TEST_F(StyledTextFieldTest, ResetFieldEditorBase) {
+ // Capture the editor frame resulting from the standard focus machinery.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ const NSRect baseEditorFrame(EditorFrame());
+
+ // Setting a hint should result in a strictly smaller editor frame.
+ EXPECT_EQ(0, [cell_ leftMargin]);
+ EXPECT_EQ(0, [cell_ rightMargin]);
+ [cell_ setLeftMargin:10];
+ [field_ resetFieldEditorFrameIfNeeded];
+ EXPECT_FALSE(NSEqualRects(baseEditorFrame, EditorFrame()));
+ EXPECT_TRUE(NSContainsRect(baseEditorFrame, EditorFrame()));
+
+ // Resetting the margin and using -resetFieldEditorFrameIfNeeded should result
+ // in the same frame as the standard focus machinery.
+ [cell_ setLeftMargin:0];
+ [field_ resetFieldEditorFrameIfNeeded];
+ EXPECT_TRUE(NSEqualRects(baseEditorFrame, EditorFrame()));
+}
+
+// Test that the field editor gets the same bounds when focus is delivered by
+// the standard focusing machinery, or by -resetFieldEditorFrameIfNeeded.
+TEST_F(StyledTextFieldTest, ResetFieldEditorLeftMargin) {
+ const CGFloat kLeftMargin = 20;
+
+ // Start the cell off with a non-zero left margin.
+ [cell_ setLeftMargin:kLeftMargin];
+ [cell_ setRightMargin:0];
+
+ // Capture the editor frame resulting from the standard focus machinery.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ const NSRect baseEditorFrame(EditorFrame());
+
+ // Clearing the margin should result in a strictly larger editor frame.
+ [cell_ setLeftMargin:0];
+ [field_ resetFieldEditorFrameIfNeeded];
+ EXPECT_FALSE(NSEqualRects(baseEditorFrame, EditorFrame()));
+ EXPECT_TRUE(NSContainsRect(EditorFrame(), baseEditorFrame));
+
+ // Setting the same margin and using -resetFieldEditorFrameIfNeeded should
+ // result in the same frame as the standard focus machinery.
+ [cell_ setLeftMargin:kLeftMargin];
+ [field_ resetFieldEditorFrameIfNeeded];
+ EXPECT_TRUE(NSEqualRects(baseEditorFrame, EditorFrame()));
+}
+
+// Test that the field editor gets the same bounds when focus is delivered by
+// the standard focusing machinery, or by -resetFieldEditorFrameIfNeeded.
+TEST_F(StyledTextFieldTest, ResetFieldEditorRightMargin) {
+ const CGFloat kRightMargin = 20;
+
+ // Start the cell off with a non-zero right margin.
+ [cell_ setLeftMargin:0];
+ [cell_ setRightMargin:kRightMargin];
+
+ // Capture the editor frame resulting from the standard focus machinery.
+ [test_window() makePretendKeyWindowAndSetFirstResponder:field_];
+ const NSRect baseEditorFrame(EditorFrame());
+
+ // Clearing the margin should result in a strictly larger editor frame.
+ [cell_ setRightMargin:0];
+ [field_ resetFieldEditorFrameIfNeeded];
+ EXPECT_FALSE(NSEqualRects(baseEditorFrame, EditorFrame()));
+ EXPECT_TRUE(NSContainsRect(EditorFrame(), baseEditorFrame));
+
+ // Setting the same margin and using -resetFieldEditorFrameIfNeeded should
+ // result in the same frame as the standard focus machinery.
+ [cell_ setRightMargin:kRightMargin];
+ [field_ resetFieldEditorFrameIfNeeded];
+ EXPECT_TRUE(NSEqualRects(baseEditorFrame, EditorFrame()));
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/tab_contents_controller.h b/chrome/browser/ui/cocoa/tab_contents_controller.h
new file mode 100644
index 0000000..f821ea4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_contents_controller.h
@@ -0,0 +1,75 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_TAB_CONTENTS_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_TAB_CONTENTS_CONTROLLER_H_
+#pragma once
+
+#include <Cocoa/Cocoa.h>
+
+#include "base/scoped_ptr.h"
+
+class TabContents;
+class TabContentsNotificationBridge;
+@class TabContentsController;
+
+// The interface for the tab contents view controller's delegate.
+
+@protocol TabContentsControllerDelegate
+
+// Tells the delegate when the tab contents view's frame is about to change.
+- (void)tabContentsViewFrameWillChange:(TabContentsController*)source
+ frameRect:(NSRect)frameRect;
+
+@end
+
+// A class that controls the TabContents view. It manages displaying the
+// native view for a given TabContents.
+// Note that just creating the class does not display the view. We defer
+// inserting it until the box is the correct size to avoid multiple resize
+// messages to the renderer. You must call |-ensureContentsVisible| to display
+// the render widget host view.
+
+@interface TabContentsController : NSViewController {
+ @private
+ TabContents* contents_; // weak
+ // Delegate to be notified about size changes.
+ id<TabContentsControllerDelegate> delegate_; // weak
+ scoped_ptr<TabContentsNotificationBridge> tabContentsBridge_;
+}
+@property(readonly, nonatomic) TabContents* tabContents;
+
+- (id)initWithContents:(TabContents*)contents
+ delegate:(id<TabContentsControllerDelegate>)delegate;
+
+// Call when the tab contents is about to be replaced with the currently
+// selected tab contents to do not trigger unnecessary content relayout.
+- (void)ensureContentsSizeDoesNotChange;
+
+// Call when the tab view is properly sized and the render widget host view
+// should be put into the view hierarchy.
+- (void)ensureContentsVisible;
+
+// Call to change the underlying tab contents object. View is not changed,
+// call |-ensureContentsVisible| to display the |newContents|'s render widget
+// host view.
+- (void)changeTabContents:(TabContents*)newContents;
+
+// Called when the tab contents is the currently selected tab and is about to be
+// removed from the view hierarchy.
+- (void)willBecomeUnselectedTab;
+
+// Called when the tab contents is about to be put into the view hierarchy as
+// the selected tab. Handles things such as ensuring the toolbar is correctly
+// enabled.
+- (void)willBecomeSelectedTab;
+
+// Called when the tab contents is updated in some non-descript way (the
+// notification from the model isn't specific). |updatedContents| could reflect
+// an entirely new tab contents object.
+- (void)tabDidChange:(TabContents*)updatedContents;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_TAB_CONTENTS_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/tab_contents_controller.mm b/chrome/browser/ui/cocoa/tab_contents_controller.mm
new file mode 100644
index 0000000..c7b5cf8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_contents_controller.mm
@@ -0,0 +1,212 @@
+// 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/ui/cocoa/tab_contents_controller.h"
+
+#include "base/mac_util.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/renderer_host/render_view_host.h"
+#include "chrome/browser/renderer_host/render_widget_host_view.h"
+#include "chrome/browser/tab_contents/navigation_controller.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/common/notification_details.h"
+#include "chrome/common/notification_observer.h"
+#include "chrome/common/notification_registrar.h"
+#include "chrome/common/notification_source.h"
+#include "chrome/common/notification_type.h"
+
+
+@interface TabContentsController(Private)
+// Forwards frame update to |delegate_| (ResizeNotificationView calls it).
+- (void)tabContentsViewFrameWillChange:(NSRect)frameRect;
+// Notification from TabContents (forwarded by TabContentsNotificationBridge).
+- (void)tabContentsRenderViewHostChanged:(RenderViewHost*)oldHost
+ newHost:(RenderViewHost*)newHost;
+@end
+
+
+// A supporting C++ bridge object to register for TabContents notifications.
+
+class TabContentsNotificationBridge : public NotificationObserver {
+ public:
+ explicit TabContentsNotificationBridge(TabContentsController* controller);
+
+ // Overriden from NotificationObserver.
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+ // Register for |contents|'s notifications, remove all prior registrations.
+ void ChangeTabContents(TabContents* contents);
+ private:
+ NotificationRegistrar registrar_;
+ TabContentsController* controller_; // weak, owns us
+};
+
+TabContentsNotificationBridge::TabContentsNotificationBridge(
+ TabContentsController* controller)
+ : controller_(controller) {
+}
+
+void TabContentsNotificationBridge::Observe(
+ NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ if (type == NotificationType::RENDER_VIEW_HOST_CHANGED) {
+ RenderViewHostSwitchedDetails* switched_details =
+ Details<RenderViewHostSwitchedDetails>(details).ptr();
+ [controller_ tabContentsRenderViewHostChanged:switched_details->old_host
+ newHost:switched_details->new_host];
+ } else {
+ NOTREACHED();
+ }
+}
+
+void TabContentsNotificationBridge::ChangeTabContents(TabContents* contents) {
+ registrar_.RemoveAll();
+ if (contents) {
+ registrar_.Add(this,
+ NotificationType::RENDER_VIEW_HOST_CHANGED,
+ Source<NavigationController>(&contents->controller()));
+ }
+}
+
+
+// A custom view that notifies |controller| that view's frame is changing.
+
+@interface ResizeNotificationView : NSView {
+ TabContentsController* controller_;
+}
+- (id)initWithController:(TabContentsController*)controller;
+@end
+
+@implementation ResizeNotificationView
+
+- (id)initWithController:(TabContentsController*)controller {
+ if ((self = [super initWithFrame:NSZeroRect])) {
+ controller_ = controller;
+ }
+ return self;
+}
+
+- (void)setFrame:(NSRect)frameRect {
+ [controller_ tabContentsViewFrameWillChange:frameRect];
+ [super setFrame:frameRect];
+}
+
+@end
+
+
+@implementation TabContentsController
+@synthesize tabContents = contents_;
+
+- (id)initWithContents:(TabContents*)contents
+ delegate:(id<TabContentsControllerDelegate>)delegate {
+ if ((self = [super initWithNibName:nil bundle:nil])) {
+ contents_ = contents;
+ delegate_ = delegate;
+ tabContentsBridge_.reset(new TabContentsNotificationBridge(self));
+ tabContentsBridge_->ChangeTabContents(contents);
+ }
+ return self;
+}
+
+- (void)dealloc {
+ // make sure our contents have been removed from the window
+ [[self view] removeFromSuperview];
+ [super dealloc];
+}
+
+- (void)loadView {
+ scoped_nsobject<ResizeNotificationView> view(
+ [[ResizeNotificationView alloc] initWithController:self]);
+ [view setAutoresizingMask:NSViewHeightSizable|NSViewWidthSizable];
+ [self setView:view];
+}
+
+- (void)ensureContentsSizeDoesNotChange {
+ if (contents_) {
+ NSView* contentsContainer = [self view];
+ NSArray* subviews = [contentsContainer subviews];
+ if ([subviews count] > 0)
+ [contents_->GetNativeView() setAutoresizingMask:NSViewNotSizable];
+ }
+}
+
+// Call when the tab view is properly sized and the render widget host view
+// should be put into the view hierarchy.
+- (void)ensureContentsVisible {
+ if (!contents_)
+ return;
+ NSView* contentsContainer = [self view];
+ NSArray* subviews = [contentsContainer subviews];
+ NSView* contentsNativeView = contents_->GetNativeView();
+
+ NSRect contentsNativeViewFrame = [contentsContainer frame];
+ contentsNativeViewFrame.origin = NSZeroPoint;
+
+ [delegate_ tabContentsViewFrameWillChange:self
+ frameRect:contentsNativeViewFrame];
+
+ // Native view is resized to the actual size before it becomes visible
+ // to avoid flickering.
+ [contentsNativeView setFrame:contentsNativeViewFrame];
+ if ([subviews count] == 0) {
+ [contentsContainer addSubview:contentsNativeView];
+ } else if ([subviews objectAtIndex:0] != contentsNativeView) {
+ [contentsContainer replaceSubview:[subviews objectAtIndex:0]
+ with:contentsNativeView];
+ }
+ // Restore autoresizing properties possibly stripped by
+ // ensureContentsSizeDoesNotChange call.
+ [contentsNativeView setAutoresizingMask:NSViewWidthSizable|
+ NSViewHeightSizable];
+}
+
+- (void)changeTabContents:(TabContents*)newContents {
+ contents_ = newContents;
+ tabContentsBridge_->ChangeTabContents(contents_);
+}
+
+- (void)tabContentsViewFrameWillChange:(NSRect)frameRect {
+ [delegate_ tabContentsViewFrameWillChange:self frameRect:frameRect];
+}
+
+- (void)tabContentsRenderViewHostChanged:(RenderViewHost*)oldHost
+ newHost:(RenderViewHost*)newHost {
+ if (oldHost && newHost && oldHost->view() && newHost->view()) {
+ newHost->view()->set_reserved_contents_rect(
+ oldHost->view()->reserved_contents_rect());
+ } else {
+ [delegate_ tabContentsViewFrameWillChange:self
+ frameRect:[[self view] frame]];
+ }
+}
+
+- (void)willBecomeUnselectedTab {
+ // The RWHV is ripped out of the view hierarchy on tab switches, so it never
+ // formally resigns first responder status. Handle this by explicitly sending
+ // a Blur() message to the renderer, but only if the RWHV currently has focus.
+ RenderViewHost* rvh = [self tabContents]->render_view_host();
+ if (rvh && rvh->view() && rvh->view()->HasFocus())
+ rvh->Blur();
+}
+
+- (void)willBecomeSelectedTab {
+ // Do not explicitly call Focus() here, as the RWHV may not actually have
+ // focus (for example, if the omnibox has focus instead). The TabContents
+ // logic will restore focus to the appropriate view.
+}
+
+- (void)tabDidChange:(TabContents*)updatedContents {
+ // Calling setContentView: here removes any first responder status
+ // the view may have, so avoid changing the view hierarchy unless
+ // the view is different.
+ if ([self tabContents] != updatedContents) {
+ [self changeTabContents:updatedContents];
+ if ([self tabContents])
+ [self ensureContentsVisible];
+ }
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/tab_controller.h b/chrome/browser/ui/cocoa/tab_controller.h
new file mode 100644
index 0000000..c85bb62
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_controller.h
@@ -0,0 +1,113 @@
+// 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_UI_COCOA_TAB_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_TAB_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+#include "chrome/browser/tab_menu_model.h"
+#import "chrome/browser/ui/cocoa/hover_close_button.h"
+
+// The loading/waiting state of the tab.
+enum TabLoadingState {
+ kTabDone,
+ kTabLoading,
+ kTabWaiting,
+ kTabCrashed,
+};
+
+@class MenuController;
+namespace TabControllerInternal {
+class MenuDelegate;
+}
+@class TabView;
+@protocol TabControllerTarget;
+
+// A class that manages a single tab in the tab strip. Set its target/action
+// to be sent a message when the tab is selected by the user clicking. Setting
+// the |loading| property to YES visually indicates that this tab is currently
+// loading content via a spinner.
+//
+// The tab has the notion of an "icon view" which can be used to display
+// identifying characteristics such as a favicon, or since it's a full-fledged
+// view, something with state and animation such as a throbber for illustrating
+// progress. The default in the nib is an image view so nothing special is
+// required if that's all you need.
+
+@interface TabController : NSViewController {
+ @private
+ IBOutlet NSView* iconView_;
+ IBOutlet NSTextField* titleView_;
+ IBOutlet HoverCloseButton* closeButton_;
+
+ NSRect originalIconFrame_; // frame of iconView_ as loaded from nib
+ BOOL isIconShowing_; // last state of iconView_ in updateVisibility
+
+ BOOL app_;
+ BOOL mini_;
+ BOOL pinned_;
+ BOOL selected_;
+ TabLoadingState loadingState_;
+ CGFloat iconTitleXOffset_; // between left edges of icon and title
+ CGFloat titleCloseWidthOffset_; // between right edges of icon and close btn.
+ id<TabControllerTarget> target_; // weak, where actions are sent
+ SEL action_; // selector sent when tab is selected by clicking
+ scoped_ptr<TabMenuModel> contextMenuModel_;
+ scoped_ptr<TabControllerInternal::MenuDelegate> contextMenuDelegate_;
+ scoped_nsobject<MenuController> contextMenuController_;
+}
+
+@property(assign, nonatomic) TabLoadingState loadingState;
+
+@property(assign, nonatomic) SEL action;
+@property(assign, nonatomic) BOOL app;
+@property(assign, nonatomic) BOOL mini;
+@property(assign, nonatomic) BOOL pinned;
+@property(assign, nonatomic) BOOL selected;
+@property(assign, nonatomic) id target;
+
+// Minimum and maximum allowable tab width. The minimum width does not show
+// the icon or the close button. The selected tab always has at least a close
+// button so it has a different minimum width.
++ (CGFloat)minTabWidth;
++ (CGFloat)maxTabWidth;
++ (CGFloat)minSelectedTabWidth;
++ (CGFloat)miniTabWidth;
++ (CGFloat)appTabWidth;
+
+// The view associated with this controller, pre-casted as a TabView
+- (TabView*)tabView;
+
+// Closes the associated TabView by relaying the message to |target_| to
+// perform the close.
+- (IBAction)closeTab:(id)sender;
+
+// Replace the current icon view with the given view. |iconView| will be
+// resized to the size of the current icon view.
+- (void)setIconView:(NSView*)iconView;
+- (NSView*)iconView;
+
+// Called by the tabs to determine whether we are in rapid (tab) closure mode.
+// In this mode, we handle clicks slightly differently due to animation.
+// Ideally, tabs would know about their own animation and wouldn't need this.
+- (BOOL)inRapidClosureMode;
+
+// Updates the visibility of certain subviews, such as the icon and close
+// button, based on criteria such as the tab's selected state and its current
+// width.
+- (void)updateVisibility;
+
+// Update the title color to match the tabs current state.
+- (void)updateTitleColor;
+@end
+
+@interface TabController(TestingAPI)
+- (NSString*)toolTip;
+- (int)iconCapacity;
+- (BOOL)shouldShowIcon;
+- (BOOL)shouldShowCloseButton;
+@end // TabController(TestingAPI)
+
+#endif // CHROME_BROWSER_UI_COCOA_TAB_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/tab_controller.mm b/chrome/browser/ui/cocoa/tab_controller.mm
new file mode 100644
index 0000000..7a6f675
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_controller.mm
@@ -0,0 +1,313 @@
+// 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 "app/l10n_util_mac.h"
+#include "base/mac_util.h"
+#import "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/menu_controller.h"
+#import "chrome/browser/ui/cocoa/tab_controller.h"
+#import "chrome/browser/ui/cocoa/tab_controller_target.h"
+#import "chrome/browser/ui/cocoa/tab_view.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#import "chrome/common/extensions/extension.h"
+#include "grit/generated_resources.h"
+
+@implementation TabController
+
+@synthesize action = action_;
+@synthesize app = app_;
+@synthesize loadingState = loadingState_;
+@synthesize mini = mini_;
+@synthesize pinned = pinned_;
+@synthesize target = target_;
+
+namespace TabControllerInternal {
+
+// A C++ delegate that handles enabling/disabling menu items and handling when
+// a menu command is chosen. Also fixes up the menu item label for "pin/unpin
+// tab".
+class MenuDelegate : public menus::SimpleMenuModel::Delegate {
+ public:
+ explicit MenuDelegate(id<TabControllerTarget> target, TabController* owner)
+ : target_(target),
+ owner_(owner) {}
+
+ // Overridden from menus::SimpleMenuModel::Delegate
+ virtual bool IsCommandIdChecked(int command_id) const { return false; }
+ virtual bool IsCommandIdEnabled(int command_id) const {
+ TabStripModel::ContextMenuCommand command =
+ static_cast<TabStripModel::ContextMenuCommand>(command_id);
+ return [target_ isCommandEnabled:command forController:owner_];
+ }
+ virtual bool GetAcceleratorForCommandId(
+ int command_id,
+ menus::Accelerator* accelerator) { return false; }
+ virtual void ExecuteCommand(int command_id) {
+ TabStripModel::ContextMenuCommand command =
+ static_cast<TabStripModel::ContextMenuCommand>(command_id);
+ [target_ commandDispatch:command forController:owner_];
+ }
+
+ private:
+ id<TabControllerTarget> target_; // weak
+ TabController* owner_; // weak, owns me
+};
+
+} // TabControllerInternal namespace
+
+// The min widths match the windows values and are sums of left + right
+// padding, of which we have no comparable constants (we draw using paths, not
+// images). The selected tab width includes the close button width.
++ (CGFloat)minTabWidth { return 31; }
++ (CGFloat)minSelectedTabWidth { return 46; }
++ (CGFloat)maxTabWidth { return 220; }
++ (CGFloat)miniTabWidth { return 53; }
++ (CGFloat)appTabWidth { return 66; }
+
+- (TabView*)tabView {
+ return static_cast<TabView*>([self view]);
+}
+
+- (id)init {
+ self = [super initWithNibName:@"TabView" bundle:mac_util::MainAppBundle()];
+ if (self != nil) {
+ isIconShowing_ = YES;
+ NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
+ [defaultCenter addObserver:self
+ selector:@selector(viewResized:)
+ name:NSViewFrameDidChangeNotification
+ object:[self view]];
+ [defaultCenter addObserver:self
+ selector:@selector(themeChangedNotification:)
+ name:kBrowserThemeDidChangeNotification
+ object:nil];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [[self tabView] setController:nil];
+ [super dealloc];
+}
+
+// The internals of |-setSelected:| but doesn't check if we're already set
+// to |selected|. Pass the selection change to the subviews that need it and
+// mark ourselves as needing a redraw.
+- (void)internalSetSelected:(BOOL)selected {
+ selected_ = selected;
+ TabView* tabView = static_cast<TabView*>([self view]);
+ DCHECK([tabView isKindOfClass:[TabView class]]);
+ [tabView setState:selected];
+ [tabView cancelAlert];
+ [self updateVisibility];
+ [self updateTitleColor];
+}
+
+// Called when the tab's nib is done loading and all outlets are hooked up.
+- (void)awakeFromNib {
+ // Remember the icon's frame, so that if the icon is ever removed, a new
+ // one can later replace it in the proper location.
+ originalIconFrame_ = [iconView_ frame];
+
+ // When the icon is removed, the title expands to the left to fill the space
+ // left by the icon. When the close button is removed, the title expands to
+ // the right to fill its space. These are the amounts to expand and contract
+ // titleView_ under those conditions.
+ NSRect titleFrame = [titleView_ frame];
+ iconTitleXOffset_ = NSMinX(titleFrame) - NSMinX(originalIconFrame_);
+ titleCloseWidthOffset_ = NSMaxX([closeButton_ frame]) - NSMaxX(titleFrame);
+
+ [self internalSetSelected:selected_];
+}
+
+// Called when Cocoa wants to display the context menu. Lazily instantiate
+// the menu based off of the cross-platform model. Re-create the menu and
+// model every time to get the correct labels and enabling.
+- (NSMenu*)menu {
+ contextMenuDelegate_.reset(
+ new TabControllerInternal::MenuDelegate(target_, self));
+ contextMenuModel_.reset(new TabMenuModel(contextMenuDelegate_.get(),
+ [self pinned]));
+ contextMenuController_.reset(
+ [[MenuController alloc] initWithModel:contextMenuModel_.get()
+ useWithPopUpButtonCell:NO]);
+ return [contextMenuController_ menu];
+}
+
+- (IBAction)closeTab:(id)sender {
+ if ([[self target] respondsToSelector:@selector(closeTab:)]) {
+ [[self target] performSelector:@selector(closeTab:)
+ withObject:[self view]];
+ }
+}
+
+- (void)setTitle:(NSString*)title {
+ [[self view] setToolTip:title];
+ if ([self mini] && ![self selected]) {
+ TabView* tabView = static_cast<TabView*>([self view]);
+ DCHECK([tabView isKindOfClass:[TabView class]]);
+ [tabView startAlert];
+ }
+ [super setTitle:title];
+}
+
+- (void)setSelected:(BOOL)selected {
+ if (selected_ != selected)
+ [self internalSetSelected:selected];
+}
+
+- (BOOL)selected {
+ return selected_;
+}
+
+- (void)setIconView:(NSView*)iconView {
+ [iconView_ removeFromSuperview];
+ iconView_ = iconView;
+ if ([self app]) {
+ NSRect appIconFrame = [iconView frame];
+ appIconFrame.origin = originalIconFrame_.origin;
+ // Center the icon.
+ appIconFrame.origin.x = ([TabController appTabWidth] -
+ NSWidth(appIconFrame)) / 2.0;
+ [iconView setFrame:appIconFrame];
+ } else {
+ [iconView_ setFrame:originalIconFrame_];
+ }
+ // Ensure that the icon is suppressed if no icon is set or if the tab is too
+ // narrow to display one.
+ [self updateVisibility];
+
+ if (iconView_)
+ [[self view] addSubview:iconView_];
+}
+
+- (NSView*)iconView {
+ return iconView_;
+}
+
+- (NSString*)toolTip {
+ return [[self view] toolTip];
+}
+
+// Return a rough approximation of the number of icons we could fit in the
+// tab. We never actually do this, but it's a helpful guide for determining
+// how much space we have available.
+- (int)iconCapacity {
+ CGFloat width = NSMaxX([closeButton_ frame]) - NSMinX(originalIconFrame_);
+ CGFloat iconWidth = NSWidth(originalIconFrame_);
+
+ return width / iconWidth;
+}
+
+// Returns YES if we should show the icon. When tabs get too small, we clip
+// the favicon before the close button for selected tabs, and prefer the
+// favicon for unselected tabs. The icon can also be suppressed more directly
+// by clearing iconView_.
+- (BOOL)shouldShowIcon {
+ if (!iconView_)
+ return NO;
+
+ if ([self mini])
+ return YES;
+
+ int iconCapacity = [self iconCapacity];
+ if ([self selected])
+ return iconCapacity >= 2;
+ return iconCapacity >= 1;
+}
+
+// Returns YES if we should be showing the close button. The selected tab
+// always shows the close button.
+- (BOOL)shouldShowCloseButton {
+ if ([self mini])
+ return NO;
+ return ([self selected] || [self iconCapacity] >= 3);
+}
+
+- (void)updateVisibility {
+ // iconView_ may have been replaced or it may be nil, so [iconView_ isHidden]
+ // won't work. Instead, the state of the icon is tracked separately in
+ // isIconShowing_.
+ BOOL oldShowIcon = isIconShowing_ ? YES : NO;
+ BOOL newShowIcon = [self shouldShowIcon] ? YES : NO;
+
+ [iconView_ setHidden:newShowIcon ? NO : YES];
+ isIconShowing_ = newShowIcon;
+
+ // If the tab is a mini-tab, hide the title.
+ [titleView_ setHidden:[self mini]];
+
+ BOOL oldShowCloseButton = [closeButton_ isHidden] ? NO : YES;
+ BOOL newShowCloseButton = [self shouldShowCloseButton] ? YES : NO;
+
+ [closeButton_ setHidden:newShowCloseButton ? NO : YES];
+
+ // Adjust the title view based on changes to the icon's and close button's
+ // visibility.
+ NSRect titleFrame = [titleView_ frame];
+
+ if (oldShowIcon != newShowIcon) {
+ // Adjust the left edge of the title view according to the presence or
+ // absence of the icon view.
+
+ if (newShowIcon) {
+ titleFrame.origin.x += iconTitleXOffset_;
+ titleFrame.size.width -= iconTitleXOffset_;
+ } else {
+ titleFrame.origin.x -= iconTitleXOffset_;
+ titleFrame.size.width += iconTitleXOffset_;
+ }
+ }
+
+ if (oldShowCloseButton != newShowCloseButton) {
+ // Adjust the right edge of the title view according to the presence or
+ // absence of the close button.
+ if (newShowCloseButton)
+ titleFrame.size.width -= titleCloseWidthOffset_;
+ else
+ titleFrame.size.width += titleCloseWidthOffset_;
+ }
+
+ [titleView_ setFrame:titleFrame];
+}
+
+- (void)updateTitleColor {
+ NSColor* titleColor = nil;
+ ThemeProvider* theme = [[[self view] window] themeProvider];
+ if (theme && ![self selected]) {
+ titleColor =
+ theme->GetNSColor(BrowserThemeProvider::COLOR_BACKGROUND_TAB_TEXT,
+ true);
+ }
+ // Default to the selected text color unless told otherwise.
+ if (theme && !titleColor) {
+ titleColor = theme->GetNSColor(BrowserThemeProvider::COLOR_TAB_TEXT,
+ true);
+ }
+ [titleView_ setTextColor:titleColor ? titleColor : [NSColor textColor]];
+}
+
+// Called when our view is resized. If it gets too small, start by hiding
+// the close button and only show it if tab is selected. Eventually, hide the
+// icon as well. We know that this is for our view because we only registered
+// for notifications from our specific view.
+- (void)viewResized:(NSNotification*)info {
+ [self updateVisibility];
+}
+
+- (void)themeChangedNotification:(NSNotification*)notification {
+ [self updateTitleColor];
+}
+
+// Called by the tabs to determine whether we are in rapid (tab) closure mode.
+- (BOOL)inRapidClosureMode {
+ if ([[self target] respondsToSelector:@selector(inRapidClosureMode)]) {
+ return [[self target] performSelector:@selector(inRapidClosureMode)] ?
+ YES : NO;
+ }
+ return NO;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/tab_controller_target.h b/chrome/browser/ui/cocoa/tab_controller_target.h
new file mode 100644
index 0000000..6eec01a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_controller_target.h
@@ -0,0 +1,27 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_TAB_CONTROLLER_TARGET_H_
+#define CHROME_BROWSER_UI_COCOA_TAB_CONTROLLER_TARGET_H_
+#pragma once
+
+#include "chrome/browser/tabs/tab_strip_model.h"
+
+@class TabController;
+
+// A protocol to be implemented by a TabController's target.
+@protocol TabControllerTarget
+- (void)selectTab:(id)sender;
+- (void)closeTab:(id)sender;
+
+// Dispatch context menu commands for the given tab controller.
+- (void)commandDispatch:(TabStripModel::ContextMenuCommand)command
+ forController:(TabController*)controller;
+// Returns YES if the specificed command should be enabled for the given
+// controller.
+- (BOOL)isCommandEnabled:(TabStripModel::ContextMenuCommand)command
+ forController:(TabController*)controller;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_TAB_CONTROLLER_TARGET_H_
diff --git a/chrome/browser/ui/cocoa/tab_controller_unittest.mm b/chrome/browser/ui/cocoa/tab_controller_unittest.mm
new file mode 100644
index 0000000..e9bafe9
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_controller_unittest.mm
@@ -0,0 +1,268 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/tab_controller.h"
+#import "chrome/browser/ui/cocoa/tab_controller_target.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+// Implements the target interface for the tab, which gets sent messages when
+// the tab is clicked on by the user and when its close box is clicked.
+@interface TabControllerTestTarget : NSObject<TabControllerTarget> {
+ @private
+ bool selected_;
+ bool closed_;
+}
+- (bool)selected;
+- (bool)closed;
+@end
+
+@implementation TabControllerTestTarget
+- (bool)selected {
+ return selected_;
+}
+- (bool)closed {
+ return closed_;
+}
+- (void)selectTab:(id)sender {
+ selected_ = true;
+}
+- (void)closeTab:(id)sender {
+ closed_ = true;
+}
+- (void)mouseTimer:(NSTimer*)timer {
+ // Fire the mouseUp to break the TabView drag loop.
+ NSEvent* current = [NSApp currentEvent];
+ NSWindow* window = [timer userInfo];
+ NSEvent* up = [NSEvent mouseEventWithType:NSLeftMouseUp
+ location:[current locationInWindow]
+ modifierFlags:0
+ timestamp:[current timestamp]
+ windowNumber:[window windowNumber]
+ context:nil
+ eventNumber:0
+ clickCount:1
+ pressure:1.0];
+ [window postEvent:up atStart:YES];
+}
+- (void)commandDispatch:(TabStripModel::ContextMenuCommand)command
+ forController:(TabController*)controller {
+}
+- (BOOL)isCommandEnabled:(TabStripModel::ContextMenuCommand)command
+ forController:(TabController*)controller {
+ return NO;
+}
+@end
+
+namespace {
+
+// The dragging code in TabView makes heavy use of autorelease pools so
+// inherit from CocoaTest to have one created for us.
+class TabControllerTest : public CocoaTest {
+ public:
+ TabControllerTest() { }
+};
+
+// Tests creating the controller, sticking it in a window, and removing it.
+TEST_F(TabControllerTest, Creation) {
+ NSWindow* window = test_window();
+ scoped_nsobject<TabController> controller([[TabController alloc] init]);
+ [[window contentView] addSubview:[controller view]];
+ EXPECT_TRUE([controller tabView]);
+ EXPECT_EQ([[controller view] window], window);
+ [[controller view] display]; // Test drawing to ensure nothing leaks/crashes.
+ [[controller view] removeFromSuperview];
+}
+
+// Tests sending it a close message and ensuring that the target/action get
+// called. Mimics the user clicking on the close button in the tab.
+TEST_F(TabControllerTest, Close) {
+ NSWindow* window = test_window();
+ scoped_nsobject<TabController> controller([[TabController alloc] init]);
+ [[window contentView] addSubview:[controller view]];
+
+ scoped_nsobject<TabControllerTestTarget> target(
+ [[TabControllerTestTarget alloc] init]);
+ EXPECT_FALSE([target closed]);
+ [controller setTarget:target];
+ EXPECT_EQ(target.get(), [controller target]);
+
+ [controller closeTab:nil];
+ EXPECT_TRUE([target closed]);
+
+ [[controller view] removeFromSuperview];
+}
+
+// Tests setting the |selected| property via code.
+TEST_F(TabControllerTest, APISelection) {
+ NSWindow* window = test_window();
+ scoped_nsobject<TabController> controller([[TabController alloc] init]);
+ [[window contentView] addSubview:[controller view]];
+
+ EXPECT_FALSE([controller selected]);
+ [controller setSelected:YES];
+ EXPECT_TRUE([controller selected]);
+
+ [[controller view] removeFromSuperview];
+}
+
+// Tests that setting the title of a tab sets the tooltip as well.
+TEST_F(TabControllerTest, ToolTip) {
+ NSWindow* window = test_window();
+
+ scoped_nsobject<TabController> controller([[TabController alloc] init]);
+ [[window contentView] addSubview:[controller view]];
+
+ EXPECT_TRUE([[controller toolTip] length] == 0);
+ NSString *tooltip_string = @"Some text to use as a tab title";
+ [controller setTitle:tooltip_string];
+ EXPECT_NSEQ(tooltip_string, [controller toolTip]);
+}
+
+// Tests setting the |loading| property via code.
+TEST_F(TabControllerTest, Loading) {
+ NSWindow* window = test_window();
+ scoped_nsobject<TabController> controller([[TabController alloc] init]);
+ [[window contentView] addSubview:[controller view]];
+
+ EXPECT_EQ(kTabDone, [controller loadingState]);
+ [controller setLoadingState:kTabWaiting];
+ EXPECT_EQ(kTabWaiting, [controller loadingState]);
+ [controller setLoadingState:kTabLoading];
+ EXPECT_EQ(kTabLoading, [controller loadingState]);
+ [controller setLoadingState:kTabDone];
+ EXPECT_EQ(kTabDone, [controller loadingState]);
+
+ [[controller view] removeFromSuperview];
+}
+
+// Tests selecting the tab with the mouse click and ensuring the target/action
+// get called.
+TEST_F(TabControllerTest, UserSelection) {
+ NSWindow* window = test_window();
+
+ // Create a tab at a known location in the window that we can click on
+ // to activate selection.
+ scoped_nsobject<TabController> controller([[TabController alloc] init]);
+ [[window contentView] addSubview:[controller view]];
+ NSRect frame = [[controller view] frame];
+ frame.size.width = [TabController minTabWidth];
+ frame.origin = NSMakePoint(0, 0);
+ [[controller view] setFrame:frame];
+
+ // Set the target and action.
+ scoped_nsobject<TabControllerTestTarget> target(
+ [[TabControllerTestTarget alloc] init]);
+ EXPECT_FALSE([target selected]);
+ [controller setTarget:target];
+ [controller setAction:@selector(selectTab:)];
+ EXPECT_EQ(target.get(), [controller target]);
+ EXPECT_EQ(@selector(selectTab:), [controller action]);
+
+ // In order to track a click, we have to fake a mouse down and a mouse
+ // up, but the down goes into a tight drag loop. To break the loop, we have
+ // to fire a timer that sends a mouse up event while the "drag" is ongoing.
+ [NSTimer scheduledTimerWithTimeInterval:0.1
+ target:target.get()
+ selector:@selector(mouseTimer:)
+ userInfo:window
+ repeats:NO];
+ NSEvent* current = [NSApp currentEvent];
+ NSPoint click_point = NSMakePoint(frame.size.width / 2,
+ frame.size.height / 2);
+ NSEvent* down = [NSEvent mouseEventWithType:NSLeftMouseDown
+ location:click_point
+ modifierFlags:0
+ timestamp:[current timestamp]
+ windowNumber:[window windowNumber]
+ context:nil
+ eventNumber:0
+ clickCount:1
+ pressure:1.0];
+ [[controller view] mouseDown:down];
+
+ // Check our target was told the tab got selected.
+ EXPECT_TRUE([target selected]);
+
+ [[controller view] removeFromSuperview];
+}
+
+TEST_F(TabControllerTest, IconCapacity) {
+ NSWindow* window = test_window();
+ scoped_nsobject<TabController> controller([[TabController alloc] init]);
+ [[window contentView] addSubview:[controller view]];
+ int cap = [controller iconCapacity];
+ EXPECT_GE(cap, 1);
+
+ NSRect frame = [[controller view] frame];
+ frame.size.width += 500;
+ [[controller view] setFrame:frame];
+ int newcap = [controller iconCapacity];
+ EXPECT_GT(newcap, cap);
+}
+
+TEST_F(TabControllerTest, ShouldShowIcon) {
+ NSWindow* window = test_window();
+ scoped_nsobject<TabController> controller([[TabController alloc] init]);
+ [[window contentView] addSubview:[controller view]];
+ int cap = [controller iconCapacity];
+ EXPECT_GT(cap, 0);
+
+ // Tab is minimum width, both icon and close box should be hidden.
+ NSRect frame = [[controller view] frame];
+ frame.size.width = [TabController minTabWidth];
+ [[controller view] setFrame:frame];
+ EXPECT_FALSE([controller shouldShowIcon]);
+ EXPECT_FALSE([controller shouldShowCloseButton]);
+
+ // Setting the icon when tab is at min width should not show icon (bug 18359).
+ scoped_nsobject<NSView> newIcon(
+ [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 16, 16)]);
+ [controller setIconView:newIcon.get()];
+ EXPECT_TRUE([newIcon isHidden]);
+
+ // Tab is at selected minimum width. Since it's selected, the close box
+ // should be visible.
+ [controller setSelected:YES];
+ frame = [[controller view] frame];
+ frame.size.width = [TabController minSelectedTabWidth];
+ [[controller view] setFrame:frame];
+ EXPECT_FALSE([controller shouldShowIcon]);
+ EXPECT_TRUE([newIcon isHidden]);
+ EXPECT_TRUE([controller shouldShowCloseButton]);
+
+ // Test expanding the tab to max width and ensure the icon and close box
+ // get put back, even when de-selected.
+ frame.size.width = [TabController maxTabWidth];
+ [[controller view] setFrame:frame];
+ EXPECT_TRUE([controller shouldShowIcon]);
+ EXPECT_FALSE([newIcon isHidden]);
+ EXPECT_TRUE([controller shouldShowCloseButton]);
+ [controller setSelected:NO];
+ EXPECT_TRUE([controller shouldShowIcon]);
+ EXPECT_TRUE([controller shouldShowCloseButton]);
+
+ cap = [controller iconCapacity];
+ EXPECT_GT(cap, 0);
+}
+
+TEST_F(TabControllerTest, Menu) {
+ NSWindow* window = test_window();
+ scoped_nsobject<TabController> controller([[TabController alloc] init]);
+ [[window contentView] addSubview:[controller view]];
+ int cap = [controller iconCapacity];
+ EXPECT_GT(cap, 0);
+
+ // Asking the view for its menu should yield a valid menu.
+ NSMenu* menu = [[controller view] menu];
+ EXPECT_TRUE(menu);
+ EXPECT_GT([menu numberOfItems], 0);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/tab_strip_controller.h b/chrome/browser/ui/cocoa/tab_strip_controller.h
new file mode 100644
index 0000000..631f9cf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_strip_controller.h
@@ -0,0 +1,259 @@
+// 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_UI_COCOA_TAB_STRIP_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_TAB_STRIP_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#import "chrome/browser/ui/cocoa/tab_contents_controller.h"
+#import "chrome/browser/ui/cocoa/tab_controller_target.h"
+#import "chrome/browser/ui/cocoa/url_drop_target.h"
+#import "third_party/GTM/AppKit/GTMWindowSheetController.h"
+
+@class NewTabButton;
+@class TabContentsController;
+@class TabView;
+@class TabStripView;
+
+class Browser;
+class ConstrainedWindowMac;
+class TabStripModelObserverBridge;
+class TabStripModel;
+class TabContents;
+class ToolbarModel;
+
+// The interface for the tab strip controller's delegate.
+// Delegating TabStripModelObserverBridge's events (in lieu of directly
+// subscribing to TabStripModelObserverBridge events, as TabStripController
+// does) is necessary to guarantee a proper order of subviews layout updates,
+// otherwise it might trigger unnesessary content relayout, UI flickering etc.
+@protocol TabStripControllerDelegate
+
+// Stripped down version of TabStripModelObserverBridge:selectTabWithContents.
+- (void)onSelectTabWithContents:(TabContents*)contents;
+
+// Stripped down version of TabStripModelObserverBridge:tabReplacedWithContents.
+- (void)onReplaceTabWithContents:(TabContents*)contents;
+
+// Stripped down version of TabStripModelObserverBridge:tabChangedWithContents.
+- (void)onSelectedTabChange:(TabStripModelObserver::TabChangeType)change;
+
+// Stripped down version of TabStripModelObserverBridge:tabDetachedWithContents.
+- (void)onTabDetachedWithContents:(TabContents*)contents;
+
+@end
+
+// A class that handles managing the tab strip in a browser window. It uses
+// a supporting C++ bridge object to register for notifications from the
+// TabStripModel. The Obj-C part of this class handles drag and drop and all
+// the other Cocoa-y aspects.
+//
+// For a full description of the design, see
+// http://www.chromium.org/developers/design-documents/tab-strip-mac
+@interface TabStripController :
+ NSObject<TabControllerTarget,
+ URLDropTargetController,
+ GTMWindowSheetControllerDelegate,
+ TabContentsControllerDelegate> {
+ @protected
+ // YES if tabs are to be laid out vertically instead of horizontally.
+ BOOL verticalLayout_;
+
+ @private
+ scoped_nsobject<TabStripView> tabStripView_;
+ NSView* switchView_; // weak
+ scoped_nsobject<NSView> dragBlockingView_; // avoid bad window server drags
+ NewTabButton* newTabButton_; // weak, obtained from the nib.
+
+ // Tracks the newTabButton_ for rollovers.
+ scoped_nsobject<NSTrackingArea> newTabTrackingArea_;
+ scoped_ptr<TabStripModelObserverBridge> bridge_;
+ Browser* browser_; // weak
+ TabStripModel* tabStripModel_; // weak
+ // Delegate that is informed about tab state changes.
+ id<TabStripControllerDelegate> delegate_; // weak
+
+ // YES if the new tab button is currently displaying the hover image (if the
+ // mouse is currently over the button).
+ BOOL newTabButtonShowingHoverImage_;
+
+ // Access to the TabContentsControllers (which own the parent view
+ // for the toolbar and associated tab contents) given an index. Call
+ // |indexFromModelIndex:| to convert a |tabStripModel_| index to a
+ // |tabContentsArray_| index. Do NOT assume that the indices of
+ // |tabStripModel_| and this array are identical, this is e.g. not true while
+ // tabs are animating closed (closed tabs are removed from |tabStripModel_|
+ // immediately, but from |tabContentsArray_| only after their close animation
+ // has completed).
+ scoped_nsobject<NSMutableArray> tabContentsArray_;
+ // An array of TabControllers which manage the actual tab views. See note
+ // above |tabContentsArray_|. |tabContentsArray_| and |tabArray_| always
+ // contain objects belonging to the same tabs at the same indices.
+ scoped_nsobject<NSMutableArray> tabArray_;
+
+ // Set of TabControllers that are currently animating closed.
+ scoped_nsobject<NSMutableSet> closingControllers_;
+
+ // These values are only used during a drag, and override tab positioning.
+ TabView* placeholderTab_; // weak. Tab being dragged
+ NSRect placeholderFrame_; // Frame to use
+ CGFloat placeholderStretchiness_; // Vertical force shown by streching tab.
+ NSRect droppedTabFrame_; // Initial frame of a dropped tab, for animation.
+ // Frame targets for all the current views.
+ // target frames are used because repeated requests to [NSView animator].
+ // aren't coalesced, so we store frames to avoid redundant calls.
+ scoped_nsobject<NSMutableDictionary> targetFrames_;
+ NSRect newTabTargetFrame_;
+ // If YES, do not show the new tab button during layout.
+ BOOL forceNewTabButtonHidden_;
+ // YES if we've successfully completed the initial layout. When this is
+ // NO, we probably don't want to do any animation because we're just coming
+ // into being.
+ BOOL initialLayoutComplete_;
+
+ // Width available for resizing the tabs (doesn't include the new tab
+ // button). Used to restrict the available width when closing many tabs at
+ // once to prevent them from resizing to fit the full width. If the entire
+ // width should be used, this will have a value of |kUseFullAvailableWidth|.
+ float availableResizeWidth_;
+ // A tracking area that's the size of the tab strip used to be notified
+ // when the mouse moves in the tab strip
+ scoped_nsobject<NSTrackingArea> trackingArea_;
+ TabView* hoveredTab_; // weak. Tab that the mouse is hovering over
+
+ // Array of subviews which are permanent (and which should never be removed),
+ // such as the new-tab button, but *not* the tabs themselves.
+ scoped_nsobject<NSMutableArray> permanentSubviews_;
+
+ // The default favicon, so we can use one copy for all buttons.
+ scoped_nsobject<NSImage> defaultFavIcon_;
+
+ // The amount by which to indent the tabs on the left (to make room for the
+ // red/yellow/green buttons).
+ CGFloat indentForControls_;
+
+ // Manages per-tab sheets.
+ scoped_nsobject<GTMWindowSheetController> sheetController_;
+
+ // Is the mouse currently inside the strip;
+ BOOL mouseInside_;
+}
+
+@property(nonatomic) CGFloat indentForControls;
+
+// Initialize the controller with a view and browser that contains
+// everything else we'll need. |switchView| is the view whose contents get
+// "switched" every time the user switches tabs. The children of this view
+// will be released, so if you want them to stay around, make sure
+// you have retained them.
+// |delegate| is the one listening to filtered TabStripModelObserverBridge's
+// events (see TabStripControllerDelegate for more details).
+- (id)initWithView:(TabStripView*)view
+ switchView:(NSView*)switchView
+ browser:(Browser*)browser
+ delegate:(id<TabStripControllerDelegate>)delegate;
+
+// Return the view for the currently selected tab.
+- (NSView*)selectedTabView;
+
+// Set the frame of the selected tab, also updates the internal frame dict.
+- (void)setFrameOfSelectedTab:(NSRect)frame;
+
+// Move the given tab at index |from| in this window to the location of the
+// current placeholder.
+- (void)moveTabFromIndex:(NSInteger)from;
+
+// Drop a given TabContents at the location of the current placeholder. If there
+// is no placeholder, it will go at the end. Used when dragging from another
+// window when we don't have access to the TabContents as part of our strip.
+// |frame| is in the coordinate system of the tab strip view and represents
+// where the user dropped the new tab so it can be animated into its correct
+// location when the tab is added to the model. If the tab was pinned in its
+// previous window, setting |pinned| to YES will propagate that state to the
+// new window. Mini-tabs are either app or pinned tabs; the app state is stored
+// by the |contents|, but the |pinned| state is the caller's responsibility.
+- (void)dropTabContents:(TabContentsWrapper*)contents
+ withFrame:(NSRect)frame
+ asPinnedTab:(BOOL)pinned;
+
+// Returns the index of the subview |view|. Returns -1 if not present. Takes
+// closing tabs into account such that this index will correctly match the tab
+// model. If |view| is in the process of closing, returns -1, as closing tabs
+// are no longer in the model.
+- (NSInteger)modelIndexForTabView:(NSView*)view;
+
+// Return the view at a given index.
+- (NSView*)viewAtIndex:(NSUInteger)index;
+
+// Return the number of tab views in the tab strip. It's same as number of tabs
+// in the model, except when a tab is closing, which will be counted in views
+// count, but no longer in the model.
+- (NSUInteger)viewsCount;
+
+// Set the placeholder for a dragged tab, allowing the |frame| and |strechiness|
+// to be specified. This causes this tab to be rendered in an arbitrary position
+- (void)insertPlaceholderForTab:(TabView*)tab
+ frame:(NSRect)frame
+ yStretchiness:(CGFloat)yStretchiness;
+
+// Returns whether or not |tab| can still be fully seen in the tab strip or if
+// its current position would cause it be obscured by things such as the edge
+// of the window or the window decorations. Returns YES only if the entire tab
+// is visible.
+- (BOOL)isTabFullyVisible:(TabView*)tab;
+
+// Show or hide the new tab button. The button is hidden immediately, but
+// waits until the next call to |-layoutTabs| to show it again.
+- (void)showNewTabButton:(BOOL)show;
+
+// Force the tabs to rearrange themselves to reflect the current model.
+- (void)layoutTabs;
+
+// Are we in rapid (tab) closure mode? I.e., is a full layout deferred (while
+// the user closes tabs)? Needed to overcome missing clicks during rapid tab
+// closure.
+- (BOOL)inRapidClosureMode;
+
+// Returns YES if the user is allowed to drag tabs on the strip at this moment.
+// For example, this returns NO if there are any pending tab close animtations.
+- (BOOL)tabDraggingAllowed;
+
+// Default height for tabs.
++ (CGFloat)defaultTabHeight;
+
+// Default indentation for tabs (see |indentForControls_|).
++ (CGFloat)defaultIndentForControls;
+
+// Returns the (lazily created) window sheet controller of this window. Used
+// for the per-tab sheets.
+- (GTMWindowSheetController*)sheetController;
+
+// Destroys the window sheet controller of this window, if it exists. The sheet
+// controller can be recreated by a subsequent call to |-sheetController|. Must
+// not be called if any sheets are currently open.
+// TODO(viettrungluu): This is temporary code needed to allow sheets to work
+// (read: not crash) in fullscreen mode. Once GTMWindowSheetController is
+// modified to support moving sheets between windows, this code can go away.
+// http://crbug.com/19093.
+- (void)destroySheetController;
+
+// Returns the currently active TabContentsController.
+- (TabContentsController*)activeTabContentsController;
+
+ // See comments in browser_window_controller.h for documentation about these
+ // functions.
+- (void)attachConstrainedWindow:(ConstrainedWindowMac*)window;
+- (void)removeConstrainedWindow:(ConstrainedWindowMac*)window;
+
+@end
+
+// Notification sent when the number of tabs changes. The object will be this
+// controller.
+extern NSString* const kTabStripNumberOfTabsChanged;
+
+#endif // CHROME_BROWSER_UI_COCOA_TAB_STRIP_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/tab_strip_controller.mm b/chrome/browser/ui/cocoa/tab_strip_controller.mm
new file mode 100644
index 0000000..7a34c58
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_strip_controller.mm
@@ -0,0 +1,1879 @@
+// 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/ui/cocoa/tab_strip_controller.h"
+
+#import <QuartzCore/QuartzCore.h>
+
+#include <limits>
+#include <string>
+
+#include "app/l10n_util.h"
+#include "app/resource_bundle.h"
+#include "base/mac_util.h"
+#include "base/nsimage_cache_mac.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/find_bar.h"
+#include "chrome/browser/find_bar_controller.h"
+#include "chrome/browser/metrics/user_metrics.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/debugger/devtools_window.h"
+#include "chrome/browser/net/url_fixer_upper.h"
+#include "chrome/browser/sidebar/sidebar_container.h"
+#include "chrome/browser/sidebar/sidebar_manager.h"
+#include "chrome/browser/tab_contents/navigation_controller.h"
+#include "chrome/browser/tab_contents/navigation_entry.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents/tab_contents_view.h"
+#include "chrome/browser/tab_contents_wrapper.h"
+#include "chrome/browser/tabs/tab_strip_model.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_navigator.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/constrained_window_mac.h"
+#import "chrome/browser/ui/cocoa/new_tab_button.h"
+#import "chrome/browser/ui/cocoa/tab_strip_view.h"
+#import "chrome/browser/ui/cocoa/tab_contents_controller.h"
+#import "chrome/browser/ui/cocoa/tab_controller.h"
+#import "chrome/browser/ui/cocoa/tab_strip_model_observer_bridge.h"
+#import "chrome/browser/ui/cocoa/tab_view.h"
+#import "chrome/browser/ui/cocoa/throbber_view.h"
+#include "grit/app_resources.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+
+NSString* const kTabStripNumberOfTabsChanged = @"kTabStripNumberOfTabsChanged";
+
+namespace {
+
+// The images names used for different states of the new tab button.
+NSString* const kNewTabHoverImage = @"newtab_h.pdf";
+NSString* const kNewTabImage = @"newtab.pdf";
+NSString* const kNewTabPressedImage = @"newtab_p.pdf";
+
+// A value to indicate tab layout should use the full available width of the
+// view.
+const CGFloat kUseFullAvailableWidth = -1.0;
+
+// The amount by which tabs overlap.
+const CGFloat kTabOverlap = 20.0;
+
+// The width and height for a tab's icon.
+const CGFloat kIconWidthAndHeight = 16.0;
+
+// The amount by which the new tab button is offset (from the tabs).
+const CGFloat kNewTabButtonOffset = 8.0;
+
+// The amount by which to shrink the tab strip (on the right) when the
+// incognito badge is present.
+const CGFloat kIncognitoBadgeTabStripShrink = 18;
+
+// Time (in seconds) in which tabs animate to their final position.
+const NSTimeInterval kAnimationDuration = 0.125;
+
+// Helper class for doing NSAnimationContext calls that takes a bool to disable
+// all the work. Useful for code that wants to conditionally animate.
+class ScopedNSAnimationContextGroup {
+ public:
+ explicit ScopedNSAnimationContextGroup(bool animate)
+ : animate_(animate) {
+ if (animate_) {
+ [NSAnimationContext beginGrouping];
+ }
+ }
+
+ ~ScopedNSAnimationContextGroup() {
+ if (animate_) {
+ [NSAnimationContext endGrouping];
+ }
+ }
+
+ void SetCurrentContextDuration(NSTimeInterval duration) {
+ if (animate_) {
+ [[NSAnimationContext currentContext] gtm_setDuration:duration
+ eventMask:NSLeftMouseUpMask];
+ }
+ }
+
+ void SetCurrentContextShortestDuration() {
+ if (animate_) {
+ // The minimum representable time interval. This used to stop an
+ // in-progress animation as quickly as possible.
+ const NSTimeInterval kMinimumTimeInterval =
+ std::numeric_limits<NSTimeInterval>::min();
+ // Directly set the duration to be short, avoiding the Steve slowmotion
+ // ettect the gtm_setDuration: provides.
+ [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
+ }
+ }
+
+private:
+ bool animate_;
+ DISALLOW_COPY_AND_ASSIGN(ScopedNSAnimationContextGroup);
+};
+
+} // namespace
+
+@interface TabStripController (Private)
+- (void)installTrackingArea;
+- (void)addSubviewToPermanentList:(NSView*)aView;
+- (void)regenerateSubviewList;
+- (NSInteger)indexForContentsView:(NSView*)view;
+- (void)updateFavIconForContents:(TabContents*)contents
+ atIndex:(NSInteger)modelIndex;
+- (void)layoutTabsWithAnimation:(BOOL)animate
+ regenerateSubviews:(BOOL)doUpdate;
+- (void)animationDidStopForController:(TabController*)controller
+ finished:(BOOL)finished;
+- (NSInteger)indexFromModelIndex:(NSInteger)index;
+- (NSInteger)numberOfOpenTabs;
+- (NSInteger)numberOfOpenMiniTabs;
+- (NSInteger)numberOfOpenNonMiniTabs;
+- (void)mouseMoved:(NSEvent*)event;
+- (void)setTabTrackingAreasEnabled:(BOOL)enabled;
+- (void)droppingURLsAt:(NSPoint)point
+ givesIndex:(NSInteger*)index
+ disposition:(WindowOpenDisposition*)disposition;
+- (void)setNewTabButtonHoverState:(BOOL)showHover;
+@end
+
+// A simple view class that prevents the Window Server from dragging the area
+// behind tabs. Sometimes core animation confuses it. Unfortunately, it can also
+// falsely pick up clicks during rapid tab closure, so we have to account for
+// that.
+@interface TabStripControllerDragBlockingView : NSView {
+ TabStripController* controller_; // weak; owns us
+}
+
+- (id)initWithFrame:(NSRect)frameRect
+ controller:(TabStripController*)controller;
+@end
+
+@implementation TabStripControllerDragBlockingView
+- (BOOL)mouseDownCanMoveWindow {return NO;}
+- (void)drawRect:(NSRect)rect {}
+
+- (id)initWithFrame:(NSRect)frameRect
+ controller:(TabStripController*)controller {
+ if ((self = [super initWithFrame:frameRect]))
+ controller_ = controller;
+ return self;
+}
+
+// In "rapid tab closure" mode (i.e., the user is clicking close tab buttons in
+// rapid succession), the animations confuse Cocoa's hit testing (which appears
+// to use cached results, among other tricks), so this view can somehow end up
+// getting a mouse down event. Thus we do an explicit hit test during rapid tab
+// closure, and if we find that we got a mouse down we shouldn't have, we send
+// it off to the appropriate view.
+- (void)mouseDown:(NSEvent*)event {
+ if ([controller_ inRapidClosureMode]) {
+ NSView* superview = [self superview];
+ NSPoint hitLocation =
+ [[superview superview] convertPoint:[event locationInWindow]
+ fromView:nil];
+ NSView* hitView = [superview hitTest:hitLocation];
+ if (hitView != self) {
+ [hitView mouseDown:event];
+ return;
+ }
+ }
+ [super mouseDown:event];
+}
+@end
+
+#pragma mark -
+
+// A delegate, owned by the CAAnimation system, that is alerted when the
+// animation to close a tab is completed. Calls back to the given tab strip
+// to let it know that |controller_| is ready to be removed from the model.
+// Since we only maintain weak references, the tab strip must call -invalidate:
+// to prevent the use of dangling pointers.
+@interface TabCloseAnimationDelegate : NSObject {
+ @private
+ TabStripController* strip_; // weak; owns us indirectly
+ TabController* controller_; // weak
+}
+
+// Will tell |strip| when the animation for |controller|'s view has completed.
+// These should not be nil, and will not be retained.
+- (id)initWithTabStrip:(TabStripController*)strip
+ tabController:(TabController*)controller;
+
+// Invalidates this object so that no further calls will be made to
+// |strip_|. This should be called when |strip_| is released, to
+// prevent attempts to call into the released object.
+- (void)invalidate;
+
+// CAAnimation delegate method
+- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished;
+
+@end
+
+@implementation TabCloseAnimationDelegate
+
+- (id)initWithTabStrip:(TabStripController*)strip
+ tabController:(TabController*)controller {
+ if ((self == [super init])) {
+ DCHECK(strip && controller);
+ strip_ = strip;
+ controller_ = controller;
+ }
+ return self;
+}
+
+- (void)invalidate {
+ strip_ = nil;
+ controller_ = nil;
+}
+
+- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
+ [strip_ animationDidStopForController:controller_ finished:finished];
+}
+
+@end
+
+#pragma mark -
+
+// In general, there is a one-to-one correspondence between TabControllers,
+// TabViews, TabContentsControllers, and the TabContents in the TabStripModel.
+// In the steady-state, the indices line up so an index coming from the model
+// is directly mapped to the same index in the parallel arrays holding our
+// views and controllers. This is also true when new tabs are created (even
+// though there is a small period of animation) because the tab is present
+// in the model while the TabView is animating into place. As a result, nothing
+// special need be done to handle "new tab" animation.
+//
+// This all goes out the window with the "close tab" animation. The animation
+// kicks off in |-tabDetachedWithContents:atIndex:| with the notification that
+// the tab has been removed from the model. The simplest solution at this
+// point would be to remove the views and controllers as well, however once
+// the TabView is removed from the view list, the tab z-order code takes care of
+// removing it from the tab strip and we'll get no animation. That means if
+// there is to be any visible animation, the TabView needs to stay around until
+// its animation is complete. In order to maintain consistency among the
+// internal parallel arrays, this means all structures are kept around until
+// the animation completes. At this point, though, the model and our internal
+// structures are out of sync: the indices no longer line up. As a result,
+// there is a concept of a "model index" which represents an index valid in
+// the TabStripModel. During steady-state, the "model index" is just the same
+// index as our parallel arrays (as above), but during tab close animations,
+// it is different, offset by the number of tabs preceding the index which
+// are undergoing tab closing animation. As a result, the caller needs to be
+// careful to use the available conversion routines when accessing the internal
+// parallel arrays (e.g., -indexFromModelIndex:). Care also needs to be taken
+// during tab layout to ignore closing tabs in the total width calculations and
+// in individual tab positioning (to avoid moving them right back to where they
+// were).
+//
+// In order to prevent actions being taken on tabs which are closing, the tab
+// itself gets marked as such so it no longer will send back its select action
+// or allow itself to be dragged. In addition, drags on the tab strip as a
+// whole are disabled while there are tabs closing.
+
+@implementation TabStripController
+
+@synthesize indentForControls = indentForControls_;
+
+- (id)initWithView:(TabStripView*)view
+ switchView:(NSView*)switchView
+ browser:(Browser*)browser
+ delegate:(id<TabStripControllerDelegate>)delegate {
+ DCHECK(view && switchView && browser && delegate);
+ if ((self = [super init])) {
+ tabStripView_.reset([view retain]);
+ switchView_ = switchView;
+ browser_ = browser;
+ tabStripModel_ = browser_->tabstrip_model();
+ delegate_ = delegate;
+ bridge_.reset(new TabStripModelObserverBridge(tabStripModel_, self));
+ tabContentsArray_.reset([[NSMutableArray alloc] init]);
+ tabArray_.reset([[NSMutableArray alloc] init]);
+
+ // Important note: any non-tab subviews not added to |permanentSubviews_|
+ // (see |-addSubviewToPermanentList:|) will be wiped out.
+ permanentSubviews_.reset([[NSMutableArray alloc] init]);
+
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ defaultFavIcon_.reset([rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON) retain]);
+
+ [self setIndentForControls:[[self class] defaultIndentForControls]];
+
+ // TODO(viettrungluu): WTF? "For some reason, if the view is present in the
+ // nib a priori, it draws correctly. If we create it in code and add it to
+ // the tab view, it draws with all sorts of crazy artifacts."
+ newTabButton_ = [view newTabButton];
+ [self addSubviewToPermanentList:newTabButton_];
+ [newTabButton_ setTarget:nil];
+ [newTabButton_ setAction:@selector(commandDispatch:)];
+ [newTabButton_ setTag:IDC_NEW_TAB];
+ // Set the images from code because Cocoa fails to find them in our sub
+ // bundle during tests.
+ [newTabButton_ setImage:nsimage_cache::ImageNamed(kNewTabImage)];
+ [newTabButton_
+ setAlternateImage:nsimage_cache::ImageNamed(kNewTabPressedImage)];
+ newTabButtonShowingHoverImage_ = NO;
+ newTabTrackingArea_.reset(
+ [[NSTrackingArea alloc] initWithRect:[newTabButton_ bounds]
+ options:(NSTrackingMouseEnteredAndExited |
+ NSTrackingActiveAlways)
+ owner:self
+ userInfo:nil]);
+ [newTabButton_ addTrackingArea:newTabTrackingArea_.get()];
+ targetFrames_.reset([[NSMutableDictionary alloc] init]);
+
+ dragBlockingView_.reset(
+ [[TabStripControllerDragBlockingView alloc] initWithFrame:NSZeroRect
+ controller:self]);
+ [self addSubviewToPermanentList:dragBlockingView_];
+
+ newTabTargetFrame_ = NSMakeRect(0, 0, 0, 0);
+ availableResizeWidth_ = kUseFullAvailableWidth;
+
+ closingControllers_.reset([[NSMutableSet alloc] init]);
+
+ // Install the permanent subviews.
+ [self regenerateSubviewList];
+
+ // Watch for notifications that the tab strip view has changed size so
+ // we can tell it to layout for the new size.
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(tabViewFrameChanged:)
+ name:NSViewFrameDidChangeNotification
+ object:tabStripView_];
+
+ trackingArea_.reset([[NSTrackingArea alloc]
+ initWithRect:NSZeroRect // Ignored by NSTrackingInVisibleRect
+ options:NSTrackingMouseEnteredAndExited |
+ NSTrackingMouseMoved |
+ NSTrackingActiveAlways |
+ NSTrackingInVisibleRect
+ owner:self
+ userInfo:nil]);
+ [tabStripView_ addTrackingArea:trackingArea_.get()];
+
+ // Check to see if the mouse is currently in our bounds so we can
+ // enable the tracking areas. Otherwise we won't get hover states
+ // or tab gradients if we load the window up under the mouse.
+ NSPoint mouseLoc = [[view window] mouseLocationOutsideOfEventStream];
+ mouseLoc = [view convertPoint:mouseLoc fromView:nil];
+ if (NSPointInRect(mouseLoc, [view bounds])) {
+ [self setTabTrackingAreasEnabled:YES];
+ mouseInside_ = YES;
+ }
+
+ // Set accessibility descriptions. http://openradar.appspot.com/7496255
+ NSString* description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_NEWTAB);
+ [[newTabButton_ cell]
+ accessibilitySetOverrideValue:description
+ forAttribute:NSAccessibilityDescriptionAttribute];
+
+ // Controller may have been (re-)created by switching layout modes, which
+ // means the tab model is already fully formed with tabs. Need to walk the
+ // list and create the UI for each.
+ const int existingTabCount = tabStripModel_->count();
+ const TabContentsWrapper* selection =
+ tabStripModel_->GetSelectedTabContents();
+ for (int i = 0; i < existingTabCount; ++i) {
+ TabContentsWrapper* currentContents = tabStripModel_->GetTabContentsAt(i);
+ [self insertTabWithContents:currentContents
+ atIndex:i
+ inForeground:NO];
+ if (selection == currentContents) {
+ // Must manually force a selection since the model won't send
+ // selection messages in this scenario.
+ [self selectTabWithContents:currentContents
+ previousContents:NULL
+ atIndex:i
+ userGesture:NO];
+ }
+ }
+ // Don't lay out the tabs until after the controller has been fully
+ // constructed. The |verticalLayout_| flag has not been initialized by
+ // subclasses at this point, which would cause layout to potentially use
+ // the wrong mode.
+ if (existingTabCount) {
+ [self performSelectorOnMainThread:@selector(layoutTabs)
+ withObject:nil
+ waitUntilDone:NO];
+ }
+ }
+ return self;
+}
+
+- (void)dealloc {
+ if (trackingArea_.get())
+ [tabStripView_ removeTrackingArea:trackingArea_.get()];
+
+ [newTabButton_ removeTrackingArea:newTabTrackingArea_.get()];
+ // Invalidate all closing animations so they don't call back to us after
+ // we're gone.
+ for (TabController* controller in closingControllers_.get()) {
+ NSView* view = [controller view];
+ [[[view animationForKey:@"frameOrigin"] delegate] invalidate];
+ }
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
++ (CGFloat)defaultTabHeight {
+ return 25.0;
+}
+
++ (CGFloat)defaultIndentForControls {
+ // Default indentation leaves enough room so tabs don't overlap with the
+ // window controls.
+ return 64.0;
+}
+
+// Finds the TabContentsController associated with the given index into the tab
+// model and swaps out the sole child of the contentArea to display its
+// contents.
+- (void)swapInTabAtIndex:(NSInteger)modelIndex {
+ DCHECK(modelIndex >= 0 && modelIndex < tabStripModel_->count());
+ NSInteger index = [self indexFromModelIndex:modelIndex];
+ TabContentsController* controller = [tabContentsArray_ objectAtIndex:index];
+
+ // Resize the new view to fit the window. Calling |view| may lazily
+ // instantiate the TabContentsController from the nib. Until we call
+ // |-ensureContentsVisible|, the controller doesn't install the RWHVMac into
+ // the view hierarchy. This is in order to avoid sending the renderer a
+ // spurious default size loaded from the nib during the call to |-view|.
+ NSView* newView = [controller view];
+
+ // Turns content autoresizing off, so removing and inserting views won't
+ // trigger unnecessary content relayout.
+ [controller ensureContentsSizeDoesNotChange];
+
+ // Remove the old view from the view hierarchy. We know there's only one
+ // child of |switchView_| because we're the one who put it there. There
+ // may not be any children in the case of a tab that's been closed, in
+ // which case there's no swapping going on.
+ NSArray* subviews = [switchView_ subviews];
+ if ([subviews count]) {
+ NSView* oldView = [subviews objectAtIndex:0];
+ // Set newView frame to the oldVew frame to prevent NSSplitView hosting
+ // sidebar and tab content from resizing sidebar's content view.
+ // ensureContentsVisible (see below) sets content size and autoresizing
+ // properties.
+ [newView setFrame:[oldView frame]];
+ [switchView_ replaceSubview:oldView with:newView];
+ } else {
+ [newView setFrame:[switchView_ bounds]];
+ [switchView_ addSubview:newView];
+ }
+
+ // New content is in place, delegate should adjust itself accordingly.
+ [delegate_ onSelectTabWithContents:[controller tabContents]];
+
+ // It also restores content autoresizing properties.
+ [controller ensureContentsVisible];
+
+ // Make sure the new tabs's sheets are visible (necessary when a background
+ // tab opened a sheet while it was in the background and now becomes active).
+ TabContentsWrapper* newTab = tabStripModel_->GetTabContentsAt(modelIndex);
+ DCHECK(newTab);
+ if (newTab) {
+ TabContents::ConstrainedWindowList::iterator it, end;
+ end = newTab->tab_contents()->constrained_window_end();
+ NSWindowController* controller = [[newView window] windowController];
+ DCHECK([controller isKindOfClass:[BrowserWindowController class]]);
+
+ for (it = newTab->tab_contents()->constrained_window_begin();
+ it != end;
+ ++it) {
+ ConstrainedWindow* constrainedWindow = *it;
+ static_cast<ConstrainedWindowMac*>(constrainedWindow)->Realize(
+ static_cast<BrowserWindowController*>(controller));
+ }
+ }
+
+ // Tell per-tab sheet manager about currently selected tab.
+ if (sheetController_.get()) {
+ [sheetController_ setActiveView:newView];
+ }
+}
+
+// Create a new tab view and set its cell correctly so it draws the way we want
+// it to. It will be sized and positioned by |-layoutTabs| so there's no need to
+// set the frame here. This also creates the view as hidden, it will be
+// shown during layout.
+- (TabController*)newTab {
+ TabController* controller = [[[TabController alloc] init] autorelease];
+ [controller setTarget:self];
+ [controller setAction:@selector(selectTab:)];
+ [[controller view] setHidden:YES];
+
+ return controller;
+}
+
+// (Private) Returns the number of open tabs in the tab strip. This is the
+// number of TabControllers we know about (as there's a 1-to-1 mapping from
+// these controllers to a tab) less the number of closing tabs.
+- (NSInteger)numberOfOpenTabs {
+ return static_cast<NSInteger>(tabStripModel_->count());
+}
+
+// (Private) Returns the number of open, mini-tabs.
+- (NSInteger)numberOfOpenMiniTabs {
+ // Ask the model for the number of mini tabs. Note that tabs which are in
+ // the process of closing (i.e., whose controllers are in
+ // |closingControllers_|) have already been removed from the model.
+ return tabStripModel_->IndexOfFirstNonMiniTab();
+}
+
+// (Private) Returns the number of open, non-mini tabs.
+- (NSInteger)numberOfOpenNonMiniTabs {
+ NSInteger number = [self numberOfOpenTabs] - [self numberOfOpenMiniTabs];
+ DCHECK_GE(number, 0);
+ return number;
+}
+
+// Given an index into the tab model, returns the index into the tab controller
+// or tab contents controller array accounting for tabs that are currently
+// closing. For example, if there are two tabs in the process of closing before
+// |index|, this returns |index| + 2. If there are no closing tabs, this will
+// return |index|.
+- (NSInteger)indexFromModelIndex:(NSInteger)index {
+ DCHECK(index >= 0);
+ if (index < 0)
+ return index;
+
+ NSInteger i = 0;
+ for (TabController* controller in tabArray_.get()) {
+ if ([closingControllers_ containsObject:controller]) {
+ DCHECK([(TabView*)[controller view] isClosing]);
+ ++index;
+ }
+ if (i == index) // No need to check anything after, it has no effect.
+ break;
+ ++i;
+ }
+ return index;
+}
+
+
+// Returns the index of the subview |view|. Returns -1 if not present. Takes
+// closing tabs into account such that this index will correctly match the tab
+// model. If |view| is in the process of closing, returns -1, as closing tabs
+// are no longer in the model.
+- (NSInteger)modelIndexForTabView:(NSView*)view {
+ NSInteger index = 0;
+ for (TabController* current in tabArray_.get()) {
+ // If |current| is closing, skip it.
+ if ([closingControllers_ containsObject:current])
+ continue;
+ else if ([current view] == view)
+ return index;
+ ++index;
+ }
+ return -1;
+}
+
+// Returns the index of the contents subview |view|. Returns -1 if not present.
+// Takes closing tabs into account such that this index will correctly match the
+// tab model. If |view| is in the process of closing, returns -1, as closing
+// tabs are no longer in the model.
+- (NSInteger)modelIndexForContentsView:(NSView*)view {
+ NSInteger index = 0;
+ NSInteger i = 0;
+ for (TabContentsController* current in tabContentsArray_.get()) {
+ // If the TabController corresponding to |current| is closing, skip it.
+ TabController* controller = [tabArray_ objectAtIndex:i];
+ if ([closingControllers_ containsObject:controller]) {
+ ++i;
+ continue;
+ } else if ([current view] == view) {
+ return index;
+ }
+ ++index;
+ ++i;
+ }
+ return -1;
+}
+
+
+// Returns the view at the given index, using the array of TabControllers to
+// get the associated view. Returns nil if out of range.
+- (NSView*)viewAtIndex:(NSUInteger)index {
+ if (index >= [tabArray_ count])
+ return NULL;
+ return [[tabArray_ objectAtIndex:index] view];
+}
+
+- (NSUInteger)viewsCount {
+ return [tabArray_ count];
+}
+
+// Called when the user clicks a tab. Tell the model the selection has changed,
+// which feeds back into us via a notification.
+- (void)selectTab:(id)sender {
+ DCHECK([sender isKindOfClass:[NSView class]]);
+ int index = [self modelIndexForTabView:sender];
+ if (tabStripModel_->ContainsIndex(index))
+ tabStripModel_->SelectTabContentsAt(index, true);
+}
+
+// Called when the user closes a tab. Asks the model to close the tab. |sender|
+// is the TabView that is potentially going away.
+- (void)closeTab:(id)sender {
+ DCHECK([sender isKindOfClass:[TabView class]]);
+ if ([hoveredTab_ isEqual:sender]) {
+ hoveredTab_ = nil;
+ }
+
+ NSInteger index = [self modelIndexForTabView:sender];
+ if (!tabStripModel_->ContainsIndex(index))
+ return;
+
+ TabContentsWrapper* contents = tabStripModel_->GetTabContentsAt(index);
+ if (contents)
+ UserMetrics::RecordAction(UserMetricsAction("CloseTab_Mouse"),
+ contents->tab_contents()->profile());
+ const NSInteger numberOfOpenTabs = [self numberOfOpenTabs];
+ if (numberOfOpenTabs > 1) {
+ bool isClosingLastTab = index == numberOfOpenTabs - 1;
+ if (!isClosingLastTab) {
+ // Limit the width available for laying out tabs so that tabs are not
+ // resized until a later time (when the mouse leaves the tab strip).
+ // However, if the tab being closed is a pinned tab, break out of
+ // rapid-closure mode since the mouse is almost guaranteed not to be over
+ // the closebox of the adjacent tab (due to the difference in widths).
+ // TODO(pinkerton): re-visit when handling tab overflow.
+ // http://crbug.com/188
+ if (tabStripModel_->IsTabPinned(index)) {
+ availableResizeWidth_ = kUseFullAvailableWidth;
+ } else {
+ NSView* penultimateTab = [self viewAtIndex:numberOfOpenTabs - 2];
+ availableResizeWidth_ = NSMaxX([penultimateTab frame]);
+ }
+ } else {
+ // If the rightmost tab is closed, change the available width so that
+ // another tab's close button lands below the cursor (assuming the tabs
+ // are currently below their maximum width and can grow).
+ NSView* lastTab = [self viewAtIndex:numberOfOpenTabs - 1];
+ availableResizeWidth_ = NSMaxX([lastTab frame]);
+ }
+ tabStripModel_->CloseTabContentsAt(
+ index,
+ TabStripModel::CLOSE_USER_GESTURE |
+ TabStripModel::CLOSE_CREATE_HISTORICAL_TAB);
+ } else {
+ // Use the standard window close if this is the last tab
+ // this prevents the tab from being removed from the model until after
+ // the window dissapears
+ [[tabStripView_ window] performClose:nil];
+ }
+}
+
+// Dispatch context menu commands for the given tab controller.
+- (void)commandDispatch:(TabStripModel::ContextMenuCommand)command
+ forController:(TabController*)controller {
+ int index = [self modelIndexForTabView:[controller view]];
+ if (tabStripModel_->ContainsIndex(index))
+ tabStripModel_->ExecuteContextMenuCommand(index, command);
+}
+
+// Returns YES if the specificed command should be enabled for the given
+// controller.
+- (BOOL)isCommandEnabled:(TabStripModel::ContextMenuCommand)command
+ forController:(TabController*)controller {
+ int index = [self modelIndexForTabView:[controller view]];
+ if (!tabStripModel_->ContainsIndex(index))
+ return NO;
+ return tabStripModel_->IsContextMenuCommandEnabled(index, command) ? YES : NO;
+}
+
+- (void)insertPlaceholderForTab:(TabView*)tab
+ frame:(NSRect)frame
+ yStretchiness:(CGFloat)yStretchiness {
+ placeholderTab_ = tab;
+ placeholderFrame_ = frame;
+ placeholderStretchiness_ = yStretchiness;
+ [self layoutTabsWithAnimation:initialLayoutComplete_ regenerateSubviews:NO];
+}
+
+- (BOOL)isTabFullyVisible:(TabView*)tab {
+ NSRect frame = [tab frame];
+ return NSMinX(frame) >= [self indentForControls] &&
+ NSMaxX(frame) <= NSMaxX([tabStripView_ frame]);
+}
+
+- (void)showNewTabButton:(BOOL)show {
+ forceNewTabButtonHidden_ = show ? NO : YES;
+ if (forceNewTabButtonHidden_)
+ [newTabButton_ setHidden:YES];
+}
+
+// Lay out all tabs in the order of their TabContentsControllers, which matches
+// the ordering in the TabStripModel. This call isn't that expensive, though
+// it is O(n) in the number of tabs. Tabs will animate to their new position
+// if the window is visible and |animate| is YES.
+// TODO(pinkerton): Note this doesn't do too well when the number of min-sized
+// tabs would cause an overflow. http://crbug.com/188
+- (void)layoutTabsWithAnimation:(BOOL)animate
+ regenerateSubviews:(BOOL)doUpdate {
+ DCHECK([NSThread isMainThread]);
+ if (![tabArray_ count])
+ return;
+
+ const CGFloat kMaxTabWidth = [TabController maxTabWidth];
+ const CGFloat kMinTabWidth = [TabController minTabWidth];
+ const CGFloat kMinSelectedTabWidth = [TabController minSelectedTabWidth];
+ const CGFloat kMiniTabWidth = [TabController miniTabWidth];
+ const CGFloat kAppTabWidth = [TabController appTabWidth];
+
+ NSRect enclosingRect = NSZeroRect;
+ ScopedNSAnimationContextGroup mainAnimationGroup(animate);
+ mainAnimationGroup.SetCurrentContextDuration(kAnimationDuration);
+
+ // Update the current subviews and their z-order if requested.
+ if (doUpdate)
+ [self regenerateSubviewList];
+
+ // Compute the base width of tabs given how much room we're allowed. Note that
+ // mini-tabs have a fixed width. We may not be able to use the entire width
+ // if the user is quickly closing tabs. This may be negative, but that's okay
+ // (taken care of by |MAX()| when calculating tab sizes).
+ CGFloat availableSpace = 0;
+ if (verticalLayout_) {
+ availableSpace = NSHeight([tabStripView_ bounds]);
+ } else {
+ if ([self inRapidClosureMode]) {
+ availableSpace = availableResizeWidth_;
+ } else {
+ availableSpace = NSWidth([tabStripView_ frame]);
+ // Account for the new tab button and the incognito badge.
+ availableSpace -= NSWidth([newTabButton_ frame]) + kNewTabButtonOffset;
+ if (browser_->profile()->IsOffTheRecord())
+ availableSpace -= kIncognitoBadgeTabStripShrink;
+ }
+ availableSpace -= [self indentForControls];
+ }
+
+ // This may be negative, but that's okay (taken care of by |MAX()| when
+ // calculating tab sizes). "mini" tabs in horizontal mode just get a special
+ // section, they don't change size.
+ CGFloat availableSpaceForNonMini = availableSpace;
+ if (!verticalLayout_) {
+ availableSpaceForNonMini -=
+ [self numberOfOpenMiniTabs] * (kMiniTabWidth - kTabOverlap);
+ }
+
+ // Initialize |nonMiniTabWidth| in case there aren't any non-mini-tabs; this
+ // value shouldn't actually be used.
+ CGFloat nonMiniTabWidth = kMaxTabWidth;
+ const NSInteger numberOfOpenNonMiniTabs = [self numberOfOpenNonMiniTabs];
+ if (!verticalLayout_ && numberOfOpenNonMiniTabs) {
+ // Find the width of a non-mini-tab. This only applies to horizontal
+ // mode. Add in the amount we "get back" from the tabs overlapping.
+ availableSpaceForNonMini += (numberOfOpenNonMiniTabs - 1) * kTabOverlap;
+
+ // Divide up the space between the non-mini-tabs.
+ nonMiniTabWidth = availableSpaceForNonMini / numberOfOpenNonMiniTabs;
+
+ // Clamp the width between the max and min.
+ nonMiniTabWidth = MAX(MIN(nonMiniTabWidth, kMaxTabWidth), kMinTabWidth);
+ }
+
+ BOOL visible = [[tabStripView_ window] isVisible];
+
+ CGFloat offset = [self indentForControls];
+ NSUInteger i = 0;
+ bool hasPlaceholderGap = false;
+ for (TabController* tab in tabArray_.get()) {
+ // Ignore a tab that is going through a close animation.
+ if ([closingControllers_ containsObject:tab])
+ continue;
+
+ BOOL isPlaceholder = [[tab view] isEqual:placeholderTab_];
+ NSRect tabFrame = [[tab view] frame];
+ tabFrame.size.height = [[self class] defaultTabHeight] + 1;
+ if (verticalLayout_) {
+ tabFrame.origin.y = availableSpace - tabFrame.size.height - offset;
+ tabFrame.origin.x = 0;
+ } else {
+ tabFrame.origin.y = 0;
+ tabFrame.origin.x = offset;
+ }
+ // If the tab is hidden, we consider it a new tab. We make it visible
+ // and animate it in.
+ BOOL newTab = [[tab view] isHidden];
+ if (newTab) {
+ [[tab view] setHidden:NO];
+ }
+
+ if (isPlaceholder) {
+ // Move the current tab to the correct location instantly.
+ // We need a duration or else it doesn't cancel an inflight animation.
+ ScopedNSAnimationContextGroup localAnimationGroup(animate);
+ localAnimationGroup.SetCurrentContextShortestDuration();
+ if (verticalLayout_)
+ tabFrame.origin.y = availableSpace - tabFrame.size.height - offset;
+ else
+ tabFrame.origin.x = placeholderFrame_.origin.x;
+ // TODO(alcor): reenable this
+ //tabFrame.size.height += 10.0 * placeholderStretchiness_;
+ id target = animate ? [[tab view] animator] : [tab view];
+ [target setFrame:tabFrame];
+
+ // Store the frame by identifier to aviod redundant calls to animator.
+ NSValue* identifier = [NSValue valueWithPointer:[tab view]];
+ [targetFrames_ setObject:[NSValue valueWithRect:tabFrame]
+ forKey:identifier];
+ continue;
+ }
+
+ if (placeholderTab_ && !hasPlaceholderGap) {
+ const CGFloat placeholderMin =
+ verticalLayout_ ? NSMinY(placeholderFrame_) :
+ NSMinX(placeholderFrame_);
+ if (verticalLayout_) {
+ if (NSMidY(tabFrame) > placeholderMin) {
+ hasPlaceholderGap = true;
+ offset += NSHeight(placeholderFrame_);
+ tabFrame.origin.y = availableSpace - tabFrame.size.height - offset;
+ }
+ } else {
+ // If the left edge is to the left of the placeholder's left, but the
+ // mid is to the right of it slide over to make space for it.
+ if (NSMidX(tabFrame) > placeholderMin) {
+ hasPlaceholderGap = true;
+ offset += NSWidth(placeholderFrame_);
+ offset -= kTabOverlap;
+ tabFrame.origin.x = offset;
+ }
+ }
+ }
+
+ // Set the width. Selected tabs are slightly wider when things get really
+ // small and thus we enforce a different minimum width.
+ tabFrame.size.width = [tab mini] ?
+ ([tab app] ? kAppTabWidth : kMiniTabWidth) : nonMiniTabWidth;
+ if ([tab selected])
+ tabFrame.size.width = MAX(tabFrame.size.width, kMinSelectedTabWidth);
+
+ // Animate a new tab in by putting it below the horizon unless told to put
+ // it in a specific location (i.e., from a drop).
+ // TODO(pinkerton): figure out vertical tab animations.
+ if (newTab && visible && animate) {
+ if (NSEqualRects(droppedTabFrame_, NSZeroRect)) {
+ [[tab view] setFrame:NSOffsetRect(tabFrame, 0, -NSHeight(tabFrame))];
+ } else {
+ [[tab view] setFrame:droppedTabFrame_];
+ droppedTabFrame_ = NSZeroRect;
+ }
+ }
+
+ // Check the frame by identifier to avoid redundant calls to animator.
+ id frameTarget = visible && animate ? [[tab view] animator] : [tab view];
+ NSValue* identifier = [NSValue valueWithPointer:[tab view]];
+ NSValue* oldTargetValue = [targetFrames_ objectForKey:identifier];
+ if (!oldTargetValue ||
+ !NSEqualRects([oldTargetValue rectValue], tabFrame)) {
+ [frameTarget setFrame:tabFrame];
+ [targetFrames_ setObject:[NSValue valueWithRect:tabFrame]
+ forKey:identifier];
+ }
+
+ enclosingRect = NSUnionRect(tabFrame, enclosingRect);
+
+ if (verticalLayout_) {
+ offset += NSHeight(tabFrame);
+ } else {
+ offset += NSWidth(tabFrame);
+ offset -= kTabOverlap;
+ }
+ i++;
+ }
+
+ // Hide the new tab button if we're explicitly told to. It may already
+ // be hidden, doing it again doesn't hurt. Otherwise position it
+ // appropriately, showing it if necessary.
+ if (forceNewTabButtonHidden_) {
+ [newTabButton_ setHidden:YES];
+ } else {
+ NSRect newTabNewFrame = [newTabButton_ frame];
+ // We've already ensured there's enough space for the new tab button
+ // so we don't have to check it against the available space. We do need
+ // to make sure we put it after any placeholder.
+ newTabNewFrame.origin = NSMakePoint(offset, 0);
+ newTabNewFrame.origin.x = MAX(newTabNewFrame.origin.x,
+ NSMaxX(placeholderFrame_)) +
+ kNewTabButtonOffset;
+ if ([tabContentsArray_ count])
+ [newTabButton_ setHidden:NO];
+
+ if (!NSEqualRects(newTabTargetFrame_, newTabNewFrame)) {
+ // Set the new tab button image correctly based on where the cursor is.
+ NSWindow* window = [tabStripView_ window];
+ NSPoint currentMouse = [window mouseLocationOutsideOfEventStream];
+ currentMouse = [tabStripView_ convertPoint:currentMouse fromView:nil];
+
+ BOOL shouldShowHover = [newTabButton_ pointIsOverButton:currentMouse];
+ [self setNewTabButtonHoverState:shouldShowHover];
+
+ // Move the new tab button into place. We want to animate the new tab
+ // button if it's moving to the left (closing a tab), but not when it's
+ // moving to the right (inserting a new tab). If moving right, we need
+ // to use a very small duration to make sure we cancel any in-flight
+ // animation to the left.
+ if (visible && animate) {
+ ScopedNSAnimationContextGroup localAnimationGroup(true);
+ BOOL movingLeft = NSMinX(newTabNewFrame) < NSMinX(newTabTargetFrame_);
+ if (!movingLeft) {
+ localAnimationGroup.SetCurrentContextShortestDuration();
+ }
+ [[newTabButton_ animator] setFrame:newTabNewFrame];
+ newTabTargetFrame_ = newTabNewFrame;
+ } else {
+ [newTabButton_ setFrame:newTabNewFrame];
+ newTabTargetFrame_ = newTabNewFrame;
+ }
+ }
+ }
+
+ [dragBlockingView_ setFrame:enclosingRect];
+
+ // Mark that we've successfully completed layout of at least one tab.
+ initialLayoutComplete_ = YES;
+}
+
+// When we're told to layout from the public API we usually want to animate,
+// except when it's the first time.
+- (void)layoutTabs {
+ [self layoutTabsWithAnimation:initialLayoutComplete_ regenerateSubviews:YES];
+}
+
+// Handles setting the title of the tab based on the given |contents|. Uses
+// a canned string if |contents| is NULL.
+- (void)setTabTitle:(NSViewController*)tab withContents:(TabContents*)contents {
+ NSString* titleString = nil;
+ if (contents)
+ titleString = base::SysUTF16ToNSString(contents->GetTitle());
+ if (![titleString length]) {
+ titleString = l10n_util::GetNSString(IDS_BROWSER_WINDOW_MAC_TAB_UNTITLED);
+ }
+ [tab setTitle:titleString];
+}
+
+// Called when a notification is received from the model to insert a new tab
+// at |modelIndex|.
+- (void)insertTabWithContents:(TabContentsWrapper*)contents
+ atIndex:(NSInteger)modelIndex
+ inForeground:(bool)inForeground {
+ DCHECK(contents);
+ DCHECK(modelIndex == TabStripModel::kNoTab ||
+ tabStripModel_->ContainsIndex(modelIndex));
+
+ // Take closing tabs into account.
+ NSInteger index = [self indexFromModelIndex:modelIndex];
+
+ // Make a new tab. Load the contents of this tab from the nib and associate
+ // the new controller with |contents| so it can be looked up later.
+ scoped_nsobject<TabContentsController> contentsController(
+ [[TabContentsController alloc] initWithContents:contents->tab_contents()
+ delegate:self]);
+ [tabContentsArray_ insertObject:contentsController atIndex:index];
+
+ // Make a new tab and add it to the strip. Keep track of its controller.
+ TabController* newController = [self newTab];
+ [newController setMini:tabStripModel_->IsMiniTab(modelIndex)];
+ [newController setPinned:tabStripModel_->IsTabPinned(modelIndex)];
+ [newController setApp:tabStripModel_->IsAppTab(modelIndex)];
+ [tabArray_ insertObject:newController atIndex:index];
+ NSView* newView = [newController view];
+
+ // Set the originating frame to just below the strip so that it animates
+ // upwards as it's being initially layed out. Oddly, this works while doing
+ // something similar in |-layoutTabs| confuses the window server.
+ [newView setFrame:NSOffsetRect([newView frame],
+ 0, -[[self class] defaultTabHeight])];
+
+ [self setTabTitle:newController withContents:contents->tab_contents()];
+
+ // If a tab is being inserted, we can again use the entire tab strip width
+ // for layout.
+ availableResizeWidth_ = kUseFullAvailableWidth;
+
+ // We don't need to call |-layoutTabs| if the tab will be in the foreground
+ // because it will get called when the new tab is selected by the tab model.
+ // Whenever |-layoutTabs| is called, it'll also add the new subview.
+ if (!inForeground) {
+ [self layoutTabs];
+ }
+
+ // During normal loading, we won't yet have a favicon and we'll get
+ // subsequent state change notifications to show the throbber, but when we're
+ // dragging a tab out into a new window, we have to put the tab's favicon
+ // into the right state up front as we won't be told to do it from anywhere
+ // else.
+ [self updateFavIconForContents:contents->tab_contents() atIndex:modelIndex];
+
+ // Send a broadcast that the number of tabs have changed.
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kTabStripNumberOfTabsChanged
+ object:self];
+}
+
+// Called when a notification is received from the model to select a particular
+// tab. Swaps in the toolbar and content area associated with |newContents|.
+- (void)selectTabWithContents:(TabContentsWrapper*)newContents
+ previousContents:(TabContentsWrapper*)oldContents
+ atIndex:(NSInteger)modelIndex
+ userGesture:(bool)wasUserGesture {
+ // Take closing tabs into account.
+ NSInteger index = [self indexFromModelIndex:modelIndex];
+
+ if (oldContents) {
+ int oldModelIndex =
+ browser_->GetIndexOfController(&(oldContents->controller()));
+ if (oldModelIndex != -1) { // When closing a tab, the old tab may be gone.
+ NSInteger oldIndex = [self indexFromModelIndex:oldModelIndex];
+ TabContentsController* oldController =
+ [tabContentsArray_ objectAtIndex:oldIndex];
+ [oldController willBecomeUnselectedTab];
+ oldContents->view()->StoreFocus();
+ oldContents->tab_contents()->WasHidden();
+ }
+ }
+
+ // De-select all other tabs and select the new tab.
+ int i = 0;
+ for (TabController* current in tabArray_.get()) {
+ [current setSelected:(i == index) ? YES : NO];
+ ++i;
+ }
+
+ // Tell the new tab contents it is about to become the selected tab. Here it
+ // can do things like make sure the toolbar is up to date.
+ TabContentsController* newController =
+ [tabContentsArray_ objectAtIndex:index];
+ [newController willBecomeSelectedTab];
+
+ // Relayout for new tabs and to let the selected tab grow to be larger in
+ // size than surrounding tabs if the user has many. This also raises the
+ // selected tab to the top.
+ [self layoutTabs];
+
+ // Swap in the contents for the new tab.
+ [self swapInTabAtIndex:modelIndex];
+
+ if (newContents) {
+ newContents->tab_contents()->DidBecomeSelected();
+ newContents->view()->RestoreFocus();
+
+ if (newContents->tab_contents()->find_ui_active())
+ browser_->GetFindBarController()->find_bar()->SetFocusAndSelection();
+ }
+}
+
+- (void)tabReplacedWithContents:(TabContentsWrapper*)newContents
+ previousContents:(TabContentsWrapper*)oldContents
+ atIndex:(NSInteger)modelIndex {
+ NSInteger index = [self indexFromModelIndex:modelIndex];
+ TabContentsController* oldController =
+ [tabContentsArray_ objectAtIndex:index];
+ DCHECK_EQ(oldContents->tab_contents(), [oldController tabContents]);
+
+ // Simply create a new TabContentsController for |newContents| and place it
+ // into the array, replacing |oldContents|. A TabSelectedAt notification will
+ // follow, at which point we will install the new view.
+ scoped_nsobject<TabContentsController> newController(
+ [[TabContentsController alloc]
+ initWithContents:newContents->tab_contents()
+ delegate:self]);
+
+ // Bye bye, |oldController|.
+ [tabContentsArray_ replaceObjectAtIndex:index withObject:newController];
+
+ [delegate_ onReplaceTabWithContents:newContents->tab_contents()];
+
+ // Fake a tab changed notification to force tab titles and favicons to update.
+ [self tabChangedWithContents:newContents
+ atIndex:modelIndex
+ changeType:TabStripModelObserver::ALL];
+}
+
+// Remove all knowledge about this tab and its associated controller, and remove
+// the view from the strip.
+- (void)removeTab:(TabController*)controller {
+ NSUInteger index = [tabArray_ indexOfObject:controller];
+
+ // Release the tab contents controller so those views get destroyed. This
+ // will remove all the tab content Cocoa views from the hierarchy. A
+ // subsequent "select tab" notification will follow from the model. To
+ // tell us what to swap in in its absence.
+ [tabContentsArray_ removeObjectAtIndex:index];
+
+ // Remove the view from the tab strip.
+ NSView* tab = [controller view];
+ [tab removeFromSuperview];
+
+ // Remove ourself as an observer.
+ [[NSNotificationCenter defaultCenter]
+ removeObserver:self
+ name:NSViewDidUpdateTrackingAreasNotification
+ object:tab];
+
+ // Clear the tab controller's target.
+ // TODO(viettrungluu): [crbug.com/23829] Find a better way to handle the tab
+ // controller's target.
+ [controller setTarget:nil];
+
+ if ([hoveredTab_ isEqual:tab])
+ hoveredTab_ = nil;
+
+ NSValue* identifier = [NSValue valueWithPointer:tab];
+ [targetFrames_ removeObjectForKey:identifier];
+
+ // Once we're totally done with the tab, delete its controller
+ [tabArray_ removeObjectAtIndex:index];
+}
+
+// Called by the CAAnimation delegate when the tab completes the closing
+// animation.
+- (void)animationDidStopForController:(TabController*)controller
+ finished:(BOOL)finished {
+ [closingControllers_ removeObject:controller];
+ [self removeTab:controller];
+}
+
+// Save off which TabController is closing and tell its view's animator
+// where to move the tab to. Registers a delegate to call back when the
+// animation is complete in order to remove the tab from the model.
+- (void)startClosingTabWithAnimation:(TabController*)closingTab {
+ DCHECK([NSThread isMainThread]);
+ // Save off the controller into the set of animating tabs. This alerts
+ // the layout method to not do anything with it and allows us to correctly
+ // calculate offsets when working with indices into the model.
+ [closingControllers_ addObject:closingTab];
+
+ // Mark the tab as closing. This prevents it from generating any drags or
+ // selections while it's animating closed.
+ [(TabView*)[closingTab view] setClosing:YES];
+
+ // Register delegate (owned by the animation system).
+ NSView* tabView = [closingTab view];
+ CAAnimation* animation = [[tabView animationForKey:@"frameOrigin"] copy];
+ [animation autorelease];
+ scoped_nsobject<TabCloseAnimationDelegate> delegate(
+ [[TabCloseAnimationDelegate alloc] initWithTabStrip:self
+ tabController:closingTab]);
+ [animation setDelegate:delegate.get()]; // Retains delegate.
+ NSMutableDictionary* animationDictionary =
+ [NSMutableDictionary dictionaryWithDictionary:[tabView animations]];
+ [animationDictionary setObject:animation forKey:@"frameOrigin"];
+ [tabView setAnimations:animationDictionary];
+
+ // Periscope down! Animate the tab.
+ NSRect newFrame = [tabView frame];
+ newFrame = NSOffsetRect(newFrame, 0, -newFrame.size.height);
+ ScopedNSAnimationContextGroup animationGroup(true);
+ animationGroup.SetCurrentContextDuration(kAnimationDuration);
+ [[tabView animator] setFrame:newFrame];
+}
+
+// Called when a notification is received from the model that the given tab
+// has gone away. Start an animation then force a layout to put everything
+// in motion.
+- (void)tabDetachedWithContents:(TabContentsWrapper*)contents
+ atIndex:(NSInteger)modelIndex {
+ // Take closing tabs into account.
+ NSInteger index = [self indexFromModelIndex:modelIndex];
+
+ TabController* tab = [tabArray_ objectAtIndex:index];
+ if (tabStripModel_->count() > 0) {
+ [self startClosingTabWithAnimation:tab];
+ [self layoutTabs];
+ } else {
+ [self removeTab:tab];
+ }
+
+ // Send a broadcast that the number of tabs have changed.
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kTabStripNumberOfTabsChanged
+ object:self];
+
+ [delegate_ onTabDetachedWithContents:contents->tab_contents()];
+}
+
+// A helper routine for creating an NSImageView to hold the fav icon or app icon
+// for |contents|.
+- (NSImageView*)iconImageViewForContents:(TabContents*)contents {
+ BOOL isApp = contents->is_app();
+ NSImage* image = nil;
+ if (isApp) {
+ SkBitmap* icon = contents->GetExtensionAppIcon();
+ if (icon)
+ image = gfx::SkBitmapToNSImage(*icon);
+ } else {
+ image = gfx::SkBitmapToNSImage(contents->GetFavIcon());
+ }
+
+ // Either we don't have a valid favicon or there was some issue converting it
+ // from an SkBitmap. Either way, just show the default.
+ if (!image)
+ image = defaultFavIcon_.get();
+ NSRect frame = NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight);
+ NSImageView* view = [[[NSImageView alloc] initWithFrame:frame] autorelease];
+ [view setImage:image];
+ return view;
+}
+
+// Updates the current loading state, replacing the icon view with a favicon,
+// a throbber, the default icon, or nothing at all.
+- (void)updateFavIconForContents:(TabContents*)contents
+ atIndex:(NSInteger)modelIndex {
+ if (!contents)
+ return;
+
+ static NSImage* throbberWaitingImage =
+ [ResourceBundle::GetSharedInstance().GetNativeImageNamed(
+ IDR_THROBBER_WAITING) retain];
+ static NSImage* throbberLoadingImage =
+ [ResourceBundle::GetSharedInstance().GetNativeImageNamed(IDR_THROBBER)
+ retain];
+ static NSImage* sadFaviconImage =
+ [ResourceBundle::GetSharedInstance().GetNativeImageNamed(IDR_SAD_FAVICON)
+ retain];
+
+ // Take closing tabs into account.
+ NSInteger index = [self indexFromModelIndex:modelIndex];
+ TabController* tabController = [tabArray_ objectAtIndex:index];
+
+ bool oldHasIcon = [tabController iconView] != nil;
+ bool newHasIcon = contents->ShouldDisplayFavIcon() ||
+ tabStripModel_->IsMiniTab(modelIndex); // Always show icon if mini.
+
+ TabLoadingState oldState = [tabController loadingState];
+ TabLoadingState newState = kTabDone;
+ NSImage* throbberImage = nil;
+ if (contents->is_crashed()) {
+ newState = kTabCrashed;
+ newHasIcon = true;
+ } else if (contents->waiting_for_response()) {
+ newState = kTabWaiting;
+ throbberImage = throbberWaitingImage;
+ } else if (contents->is_loading()) {
+ newState = kTabLoading;
+ throbberImage = throbberLoadingImage;
+ }
+
+ if (oldState != newState)
+ [tabController setLoadingState:newState];
+
+ // While loading, this function is called repeatedly with the same state.
+ // To avoid expensive unnecessary view manipulation, only make changes when
+ // the state is actually changing. When loading is complete (kTabDone),
+ // every call to this function is significant.
+ if (newState == kTabDone || oldState != newState ||
+ oldHasIcon != newHasIcon) {
+ NSView* iconView = nil;
+ if (newHasIcon) {
+ if (newState == kTabDone) {
+ iconView = [self iconImageViewForContents:contents];
+ } else if (newState == kTabCrashed) {
+ NSImage* oldImage = [[self iconImageViewForContents:contents] image];
+ NSRect frame =
+ NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight);
+ iconView = [ThrobberView toastThrobberViewWithFrame:frame
+ beforeImage:oldImage
+ afterImage:sadFaviconImage];
+ } else {
+ NSRect frame =
+ NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight);
+ iconView = [ThrobberView filmstripThrobberViewWithFrame:frame
+ image:throbberImage];
+ }
+ }
+
+ [tabController setIconView:iconView];
+ }
+}
+
+// Called when a notification is received from the model that the given tab
+// has been updated. |loading| will be YES when we only want to update the
+// throbber state, not anything else about the (partially) loading tab.
+- (void)tabChangedWithContents:(TabContentsWrapper*)contents
+ atIndex:(NSInteger)modelIndex
+ changeType:(TabStripModelObserver::TabChangeType)change {
+ // Take closing tabs into account.
+ NSInteger index = [self indexFromModelIndex:modelIndex];
+
+ if (modelIndex == tabStripModel_->selected_index())
+ [delegate_ onSelectedTabChange:change];
+
+ if (change == TabStripModelObserver::TITLE_NOT_LOADING) {
+ // TODO(sky): make this work.
+ // We'll receive another notification of the change asynchronously.
+ return;
+ }
+
+ TabController* tabController = [tabArray_ objectAtIndex:index];
+
+ if (change != TabStripModelObserver::LOADING_ONLY)
+ [self setTabTitle:tabController withContents:contents->tab_contents()];
+
+ [self updateFavIconForContents:contents->tab_contents() atIndex:modelIndex];
+
+ TabContentsController* updatedController =
+ [tabContentsArray_ objectAtIndex:index];
+ [updatedController tabDidChange:contents->tab_contents()];
+}
+
+// Called when a tab is moved (usually by drag&drop). Keep our parallel arrays
+// in sync with the tab strip model. It can also be pinned/unpinned
+// simultaneously, so we need to take care of that.
+- (void)tabMovedWithContents:(TabContentsWrapper*)contents
+ fromIndex:(NSInteger)modelFrom
+ toIndex:(NSInteger)modelTo {
+ // Take closing tabs into account.
+ NSInteger from = [self indexFromModelIndex:modelFrom];
+ NSInteger to = [self indexFromModelIndex:modelTo];
+
+ scoped_nsobject<TabContentsController> movedTabContentsController(
+ [[tabContentsArray_ objectAtIndex:from] retain]);
+ [tabContentsArray_ removeObjectAtIndex:from];
+ [tabContentsArray_ insertObject:movedTabContentsController.get()
+ atIndex:to];
+ scoped_nsobject<TabController> movedTabController(
+ [[tabArray_ objectAtIndex:from] retain]);
+ DCHECK([movedTabController isKindOfClass:[TabController class]]);
+ [tabArray_ removeObjectAtIndex:from];
+ [tabArray_ insertObject:movedTabController.get() atIndex:to];
+
+ // The tab moved, which means that the mini-tab state may have changed.
+ if (tabStripModel_->IsMiniTab(modelTo) != [movedTabController mini])
+ [self tabMiniStateChangedWithContents:contents atIndex:modelTo];
+
+ [self layoutTabs];
+}
+
+// Called when a tab is pinned or unpinned without moving.
+- (void)tabMiniStateChangedWithContents:(TabContentsWrapper*)contents
+ atIndex:(NSInteger)modelIndex {
+ // Take closing tabs into account.
+ NSInteger index = [self indexFromModelIndex:modelIndex];
+
+ TabController* tabController = [tabArray_ objectAtIndex:index];
+ DCHECK([tabController isKindOfClass:[TabController class]]);
+
+ // Don't do anything if the change was already picked up by the move event.
+ if (tabStripModel_->IsMiniTab(modelIndex) == [tabController mini])
+ return;
+
+ [tabController setMini:tabStripModel_->IsMiniTab(modelIndex)];
+ [tabController setPinned:tabStripModel_->IsTabPinned(modelIndex)];
+ [tabController setApp:tabStripModel_->IsAppTab(modelIndex)];
+ [self updateFavIconForContents:contents->tab_contents() atIndex:modelIndex];
+ // If the tab is being restored and it's pinned, the mini state is set after
+ // the tab has already been rendered, so re-layout the tabstrip. In all other
+ // cases, the state is set before the tab is rendered so this isn't needed.
+ [self layoutTabs];
+}
+
+- (void)setFrameOfSelectedTab:(NSRect)frame {
+ NSView* view = [self selectedTabView];
+ NSValue* identifier = [NSValue valueWithPointer:view];
+ [targetFrames_ setObject:[NSValue valueWithRect:frame]
+ forKey:identifier];
+ [view setFrame:frame];
+}
+
+- (NSView*)selectedTabView {
+ int selectedIndex = tabStripModel_->selected_index();
+ // Take closing tabs into account. They can't ever be selected.
+ selectedIndex = [self indexFromModelIndex:selectedIndex];
+ return [self viewAtIndex:selectedIndex];
+}
+
+// Find the model index based on the x coordinate of the placeholder. If there
+// is no placeholder, this returns the end of the tab strip. Closing tabs are
+// not considered in computing the index.
+- (int)indexOfPlaceholder {
+ double placeholderX = placeholderFrame_.origin.x;
+ int index = 0;
+ int location = 0;
+ // Use |tabArray_| here instead of the tab strip count in order to get the
+ // correct index when there are closing tabs to the left of the placeholder.
+ const int count = [tabArray_ count];
+ while (index < count) {
+ // Ignore closing tabs for simplicity. The only drawback of this is that
+ // if the placeholder is placed right before one or several contiguous
+ // currently closing tabs, the associated TabController will start at the
+ // end of the closing tabs.
+ if ([closingControllers_ containsObject:[tabArray_ objectAtIndex:index]]) {
+ index++;
+ continue;
+ }
+ NSView* curr = [self viewAtIndex:index];
+ // The placeholder tab works by changing the frame of the tab being dragged
+ // to be the bounds of the placeholder, so we need to skip it while we're
+ // iterating, otherwise we'll end up off by one. Note This only effects
+ // dragging to the right, not to the left.
+ if (curr == placeholderTab_) {
+ index++;
+ continue;
+ }
+ if (placeholderX <= NSMinX([curr frame]))
+ break;
+ index++;
+ location++;
+ }
+ return location;
+}
+
+// Move the given tab at index |from| in this window to the location of the
+// current placeholder.
+- (void)moveTabFromIndex:(NSInteger)from {
+ int toIndex = [self indexOfPlaceholder];
+ tabStripModel_->MoveTabContentsAt(from, toIndex, true);
+}
+
+// Drop a given TabContents at the location of the current placeholder. If there
+// is no placeholder, it will go at the end. Used when dragging from another
+// window when we don't have access to the TabContents as part of our strip.
+// |frame| is in the coordinate system of the tab strip view and represents
+// where the user dropped the new tab so it can be animated into its correct
+// location when the tab is added to the model. If the tab was pinned in its
+// previous window, setting |pinned| to YES will propagate that state to the
+// new window. Mini-tabs are either app or pinned tabs; the app state is stored
+// by the |contents|, but the |pinned| state is the caller's responsibility.
+- (void)dropTabContents:(TabContentsWrapper*)contents
+ withFrame:(NSRect)frame
+ asPinnedTab:(BOOL)pinned {
+ int modelIndex = [self indexOfPlaceholder];
+
+ // Mark that the new tab being created should start at |frame|. It will be
+ // reset as soon as the tab has been positioned.
+ droppedTabFrame_ = frame;
+
+ // Insert it into this tab strip. We want it in the foreground and to not
+ // inherit the current tab's group.
+ tabStripModel_->InsertTabContentsAt(
+ modelIndex, contents,
+ TabStripModel::ADD_SELECTED | (pinned ? TabStripModel::ADD_PINNED : 0));
+}
+
+// Called when the tab strip view changes size. As we only registered for
+// changes on our view, we know it's only for our view. Layout w/out
+// animations since they are blocked by the resize nested runloop. We need
+// the views to adjust immediately. Neither the tabs nor their z-order are
+// changed, so we don't need to update the subviews.
+- (void)tabViewFrameChanged:(NSNotification*)info {
+ [self layoutTabsWithAnimation:NO regenerateSubviews:NO];
+}
+
+// Called when the tracking areas for any given tab are updated. This allows
+// the individual tabs to update their hover states correctly.
+// Only generates the event if the cursor is in the tab strip.
+- (void)tabUpdateTracking:(NSNotification*)notification {
+ DCHECK([[notification object] isKindOfClass:[TabView class]]);
+ DCHECK(mouseInside_);
+ NSWindow* window = [tabStripView_ window];
+ NSPoint location = [window mouseLocationOutsideOfEventStream];
+ if (NSPointInRect(location, [tabStripView_ frame])) {
+ NSEvent* mouseEvent = [NSEvent mouseEventWithType:NSMouseMoved
+ location:location
+ modifierFlags:0
+ timestamp:0
+ windowNumber:[window windowNumber]
+ context:nil
+ eventNumber:0
+ clickCount:0
+ pressure:0];
+ [self mouseMoved:mouseEvent];
+ }
+}
+
+- (BOOL)inRapidClosureMode {
+ return availableResizeWidth_ != kUseFullAvailableWidth;
+}
+
+// Disable tab dragging when there are any pending animations.
+- (BOOL)tabDraggingAllowed {
+ return [closingControllers_ count] == 0;
+}
+
+- (void)mouseMoved:(NSEvent*)event {
+ // Use hit test to figure out what view we are hovering over.
+ NSView* targetView = [tabStripView_ hitTest:[event locationInWindow]];
+
+ // Set the new tab button hover state iff the mouse is over the button.
+ BOOL shouldShowHoverImage = [targetView isKindOfClass:[NewTabButton class]];
+ [self setNewTabButtonHoverState:shouldShowHoverImage];
+
+ TabView* tabView = (TabView*)targetView;
+ if (![tabView isKindOfClass:[TabView class]]) {
+ if ([[tabView superview] isKindOfClass:[TabView class]]) {
+ tabView = (TabView*)[targetView superview];
+ } else {
+ tabView = nil;
+ }
+ }
+
+ if (hoveredTab_ != tabView) {
+ [hoveredTab_ mouseExited:nil]; // We don't pass event because moved events
+ [tabView mouseEntered:nil]; // don't have valid tracking areas
+ hoveredTab_ = tabView;
+ } else {
+ [hoveredTab_ mouseMoved:event];
+ }
+}
+
+- (void)mouseEntered:(NSEvent*)event {
+ NSTrackingArea* area = [event trackingArea];
+ if ([area isEqual:trackingArea_]) {
+ mouseInside_ = YES;
+ [self setTabTrackingAreasEnabled:YES];
+ [self mouseMoved:event];
+ }
+}
+
+// Called when the tracking area is in effect which means we're tracking to
+// see if the user leaves the tab strip with their mouse. When they do,
+// reset layout to use all available width.
+- (void)mouseExited:(NSEvent*)event {
+ NSTrackingArea* area = [event trackingArea];
+ if ([area isEqual:trackingArea_]) {
+ mouseInside_ = NO;
+ [self setTabTrackingAreasEnabled:NO];
+ availableResizeWidth_ = kUseFullAvailableWidth;
+ [hoveredTab_ mouseExited:event];
+ hoveredTab_ = nil;
+ [self layoutTabs];
+ } else if ([area isEqual:newTabTrackingArea_]) {
+ // If the mouse is moved quickly enough, it is possible for the mouse to
+ // leave the tabstrip without sending any mouseMoved: messages at all.
+ // Since this would result in the new tab button incorrectly staying in the
+ // hover state, disable the hover image on every mouse exit.
+ [self setNewTabButtonHoverState:NO];
+ }
+}
+
+// Enable/Disable the tracking areas for the tabs. They are only enabled
+// when the mouse is in the tabstrip.
+- (void)setTabTrackingAreasEnabled:(BOOL)enabled {
+ NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
+ for (TabController* controller in tabArray_.get()) {
+ TabView* tabView = [controller tabView];
+ if (enabled) {
+ // Set self up to observe tabs so hover states will be correct.
+ [defaultCenter addObserver:self
+ selector:@selector(tabUpdateTracking:)
+ name:NSViewDidUpdateTrackingAreasNotification
+ object:tabView];
+ } else {
+ [defaultCenter removeObserver:self
+ name:NSViewDidUpdateTrackingAreasNotification
+ object:tabView];
+ }
+ [tabView setTrackingEnabled:enabled];
+ }
+}
+
+// Sets the new tab button's image based on the current hover state. Does
+// nothing if the hover state is already correct.
+- (void)setNewTabButtonHoverState:(BOOL)shouldShowHover {
+ if (shouldShowHover && !newTabButtonShowingHoverImage_) {
+ newTabButtonShowingHoverImage_ = YES;
+ [newTabButton_ setImage:nsimage_cache::ImageNamed(kNewTabHoverImage)];
+ } else if (!shouldShowHover && newTabButtonShowingHoverImage_) {
+ newTabButtonShowingHoverImage_ = NO;
+ [newTabButton_ setImage:nsimage_cache::ImageNamed(kNewTabImage)];
+ }
+}
+
+// Adds the given subview to (the end of) the list of permanent subviews
+// (specified from bottom up). These subviews will always be below the
+// transitory subviews (tabs). |-regenerateSubviewList| must be called to
+// effectuate the addition.
+- (void)addSubviewToPermanentList:(NSView*)aView {
+ if (aView)
+ [permanentSubviews_ addObject:aView];
+}
+
+// Update the subviews, keeping the permanent ones (or, more correctly, putting
+// in the ones listed in permanentSubviews_), and putting in the current tabs in
+// the correct z-order. Any current subviews which is neither in the permanent
+// list nor a (current) tab will be removed. So if you add such a subview, you
+// should call |-addSubviewToPermanentList:| (or better yet, call that and then
+// |-regenerateSubviewList| to actually add it).
+- (void)regenerateSubviewList {
+ // Remove self as an observer from all the old tabs before a new set of
+ // potentially different tabs is put in place.
+ [self setTabTrackingAreasEnabled:NO];
+
+ // Subviews to put in (in bottom-to-top order), beginning with the permanent
+ // ones.
+ NSMutableArray* subviews = [NSMutableArray arrayWithArray:permanentSubviews_];
+
+ NSView* selectedTabView = nil;
+ // Go through tabs in reverse order, since |subviews| is bottom-to-top.
+ for (TabController* tab in [tabArray_ reverseObjectEnumerator]) {
+ NSView* tabView = [tab view];
+ if ([tab selected]) {
+ DCHECK(!selectedTabView);
+ selectedTabView = tabView;
+ } else {
+ [subviews addObject:tabView];
+ }
+ }
+ if (selectedTabView) {
+ [subviews addObject:selectedTabView];
+ }
+ [tabStripView_ setSubviews:subviews];
+ [self setTabTrackingAreasEnabled:mouseInside_];
+}
+
+// Get the index and disposition for a potential URL(s) drop given a point (in
+// the |TabStripView|'s coordinates). It considers only the x-coordinate of the
+// given point. If it's in the "middle" of a tab, it drops on that tab. If it's
+// to the left, it inserts to the left, and similarly for the right.
+- (void)droppingURLsAt:(NSPoint)point
+ givesIndex:(NSInteger*)index
+ disposition:(WindowOpenDisposition*)disposition {
+ // Proportion of the tab which is considered the "middle" (and causes things
+ // to drop on that tab).
+ const double kMiddleProportion = 0.5;
+ const double kLRProportion = (1.0 - kMiddleProportion) / 2.0;
+
+ DCHECK(index && disposition);
+ NSInteger i = 0;
+ for (TabController* tab in tabArray_.get()) {
+ NSView* view = [tab view];
+ DCHECK([view isKindOfClass:[TabView class]]);
+
+ // Recall that |-[NSView frame]| is in its superview's coordinates, so a
+ // |TabView|'s frame is in the coordinates of the |TabStripView| (which
+ // matches the coordinate system of |point|).
+ NSRect frame = [view frame];
+
+ // Modify the frame to make it "unoverlapped".
+ frame.origin.x += kTabOverlap / 2.0;
+ frame.size.width -= kTabOverlap;
+ if (frame.size.width < 1.0)
+ frame.size.width = 1.0; // try to avoid complete failure
+
+ // Drop in a new tab to the left of tab |i|?
+ if (point.x < (frame.origin.x + kLRProportion * frame.size.width)) {
+ *index = i;
+ *disposition = NEW_FOREGROUND_TAB;
+ return;
+ }
+
+ // Drop on tab |i|?
+ if (point.x <= (frame.origin.x +
+ (1.0 - kLRProportion) * frame.size.width)) {
+ *index = i;
+ *disposition = CURRENT_TAB;
+ return;
+ }
+
+ // (Dropping in a new tab to the right of tab |i| will be taken care of in
+ // the next iteration.)
+ i++;
+ }
+
+ // If we've made it here, we want to append a new tab to the end.
+ *index = -1;
+ *disposition = NEW_FOREGROUND_TAB;
+}
+
+// (URLDropTargetController protocol)
+- (void)dropURLs:(NSArray*)urls inView:(NSView*)view at:(NSPoint)point {
+ DCHECK_EQ(view, tabStripView_.get());
+
+ if ([urls count] < 1) {
+ NOTREACHED();
+ return;
+ }
+
+ //TODO(viettrungluu): dropping multiple URLs.
+ if ([urls count] > 1)
+ NOTIMPLEMENTED();
+
+ // Get the first URL and fix it up.
+ GURL url(URLFixerUpper::FixupURL(
+ base::SysNSStringToUTF8([urls objectAtIndex:0]), std::string()));
+
+ // Get the index and disposition.
+ NSInteger index;
+ WindowOpenDisposition disposition;
+ [self droppingURLsAt:point
+ givesIndex:&index
+ disposition:&disposition];
+
+ // Either insert a new tab or open in a current tab.
+ switch (disposition) {
+ case NEW_FOREGROUND_TAB: {
+ UserMetrics::RecordAction(UserMetricsAction("Tab_DropURLBetweenTabs"),
+ browser_->profile());
+ browser::NavigateParams params(browser_, url, PageTransition::TYPED);
+ params.disposition = disposition;
+ params.tabstrip_index = index;
+ params.tabstrip_add_types =
+ TabStripModel::ADD_SELECTED | TabStripModel::ADD_FORCE_INDEX;
+ browser::Navigate(&params);
+ break;
+ }
+ case CURRENT_TAB:
+ UserMetrics::RecordAction(UserMetricsAction("Tab_DropURLOnTab"),
+ browser_->profile());
+ tabStripModel_->GetTabContentsAt(index)
+ ->tab_contents()->OpenURL(url, GURL(), CURRENT_TAB,
+ PageTransition::TYPED);
+ tabStripModel_->SelectTabContentsAt(index, true);
+ break;
+ default:
+ NOTIMPLEMENTED();
+ }
+}
+
+// (URLDropTargetController protocol)
+- (void)indicateDropURLsInView:(NSView*)view at:(NSPoint)point {
+ DCHECK_EQ(view, tabStripView_.get());
+
+ // The minimum y-coordinate at which one should consider place the arrow.
+ const CGFloat arrowBaseY = 25;
+
+ NSInteger index;
+ WindowOpenDisposition disposition;
+ [self droppingURLsAt:point
+ givesIndex:&index
+ disposition:&disposition];
+
+ NSPoint arrowPos = NSMakePoint(0, arrowBaseY);
+ if (index == -1) {
+ // Append a tab at the end.
+ DCHECK(disposition == NEW_FOREGROUND_TAB);
+ NSInteger lastIndex = [tabArray_ count] - 1;
+ NSRect overRect = [[[tabArray_ objectAtIndex:lastIndex] view] frame];
+ arrowPos.x = overRect.origin.x + overRect.size.width - kTabOverlap / 2.0;
+ } else {
+ NSRect overRect = [[[tabArray_ objectAtIndex:index] view] frame];
+ switch (disposition) {
+ case NEW_FOREGROUND_TAB:
+ // Insert tab (to the left of the given tab).
+ arrowPos.x = overRect.origin.x + kTabOverlap / 2.0;
+ break;
+ case CURRENT_TAB:
+ // Overwrite the given tab.
+ arrowPos.x = overRect.origin.x + overRect.size.width / 2.0;
+ break;
+ default:
+ NOTREACHED();
+ }
+ }
+
+ [tabStripView_ setDropArrowPosition:arrowPos];
+ [tabStripView_ setDropArrowShown:YES];
+ [tabStripView_ setNeedsDisplay:YES];
+}
+
+// (URLDropTargetController protocol)
+- (void)hideDropURLsIndicatorInView:(NSView*)view {
+ DCHECK_EQ(view, tabStripView_.get());
+
+ if ([tabStripView_ dropArrowShown]) {
+ [tabStripView_ setDropArrowShown:NO];
+ [tabStripView_ setNeedsDisplay:YES];
+ }
+}
+
+- (GTMWindowSheetController*)sheetController {
+ if (!sheetController_.get())
+ sheetController_.reset([[GTMWindowSheetController alloc]
+ initWithWindow:[switchView_ window] delegate:self]);
+ return sheetController_.get();
+}
+
+- (void)destroySheetController {
+ // Make sure there are no open sheets.
+ DCHECK_EQ(0U, [[sheetController_ viewsWithAttachedSheets] count]);
+ sheetController_.reset();
+}
+
+// TabContentsControllerDelegate protocol.
+- (void)tabContentsViewFrameWillChange:(TabContentsController*)source
+ frameRect:(NSRect)frameRect {
+ id<TabContentsControllerDelegate> controller =
+ [[switchView_ window] windowController];
+ [controller tabContentsViewFrameWillChange:source frameRect:frameRect];
+}
+
+- (TabContentsController*)activeTabContentsController {
+ int modelIndex = tabStripModel_->selected_index();
+ if (modelIndex < 0)
+ return nil;
+ NSInteger index = [self indexFromModelIndex:modelIndex];
+ if (index < 0 ||
+ index >= (NSInteger)[tabContentsArray_ count])
+ return nil;
+ return [tabContentsArray_ objectAtIndex:index];
+}
+
+- (void)gtm_systemRequestsVisibilityForView:(NSView*)view {
+ // This implementation is required by GTMWindowSheetController.
+
+ // Raise window...
+ [[switchView_ window] makeKeyAndOrderFront:self];
+
+ // ...and raise a tab with a sheet.
+ NSInteger index = [self modelIndexForContentsView:view];
+ DCHECK(index >= 0);
+ if (index >= 0)
+ tabStripModel_->SelectTabContentsAt(index, false /* not a user gesture */);
+}
+
+- (void)attachConstrainedWindow:(ConstrainedWindowMac*)window {
+ // TODO(thakis, avi): Figure out how to make this work when tabs are dragged
+ // out or if fullscreen mode is toggled.
+
+ // View hierarchy of the contents view:
+ // NSView -- switchView, same for all tabs
+ // +- NSView -- TabContentsController's view
+ // +- TabContentsViewCocoa
+ // Changing it? Do not forget to modify removeConstrainedWindow too.
+ // We use the TabContentsController's view in |swapInTabAtIndex|, so we have
+ // to pass it to the sheet controller here.
+ NSView* tabContentsView = [window->owner()->GetNativeView() superview];
+ window->delegate()->RunSheet([self sheetController], tabContentsView);
+
+ // TODO(avi, thakis): GTMWindowSheetController has no api to move tabsheets
+ // between windows. Until then, we have to prevent having to move a tabsheet
+ // between windows, e.g. no tearing off of tabs.
+ NSInteger modelIndex = [self modelIndexForContentsView:tabContentsView];
+ NSInteger index = [self indexFromModelIndex:modelIndex];
+ BrowserWindowController* controller =
+ (BrowserWindowController*)[[switchView_ window] windowController];
+ DCHECK(controller != nil);
+ DCHECK(index >= 0);
+ if (index >= 0) {
+ [controller setTab:[self viewAtIndex:index] isDraggable:NO];
+ }
+}
+
+- (void)removeConstrainedWindow:(ConstrainedWindowMac*)window {
+ NSView* tabContentsView = [window->owner()->GetNativeView() superview];
+
+ // TODO(avi, thakis): GTMWindowSheetController has no api to move tabsheets
+ // between windows. Until then, we have to prevent having to move a tabsheet
+ // between windows, e.g. no tearing off of tabs.
+ NSInteger modelIndex = [self modelIndexForContentsView:tabContentsView];
+ NSInteger index = [self indexFromModelIndex:modelIndex];
+ BrowserWindowController* controller =
+ (BrowserWindowController*)[[switchView_ window] windowController];
+ DCHECK(index >= 0);
+ if (index >= 0) {
+ [controller setTab:[self viewAtIndex:index] isDraggable:YES];
+ }
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/tab_strip_controller_unittest.mm b/chrome/browser/ui/cocoa/tab_strip_controller_unittest.mm
new file mode 100644
index 0000000..19a781c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_strip_controller_unittest.mm
@@ -0,0 +1,177 @@
+// 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/browser_window.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents_wrapper.h"
+#include "chrome/browser/renderer_host/site_instance.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/new_tab_button.h"
+#import "chrome/browser/ui/cocoa/tab_strip_controller.h"
+#import "chrome/browser/ui/cocoa/tab_strip_view.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface TestTabStripControllerDelegate :
+ NSObject<TabStripControllerDelegate> {
+}
+@end
+
+@implementation TestTabStripControllerDelegate
+- (void)onSelectTabWithContents:(TabContents*)contents {
+}
+- (void)onReplaceTabWithContents:(TabContents*)contents {
+}
+- (void)onSelectedTabChange:(TabStripModelObserver::TabChangeType)change {
+}
+- (void)onTabDetachedWithContents:(TabContents*)contents {
+}
+@end
+
+namespace {
+
+// Stub model delegate
+class TestTabStripDelegate : public TabStripModelDelegate {
+ public:
+ virtual TabContentsWrapper* AddBlankTab(bool foreground) {
+ return NULL;
+ }
+ virtual TabContentsWrapper* AddBlankTabAt(int index, bool foreground) {
+ return NULL;
+ }
+ virtual Browser* CreateNewStripWithContents(TabContentsWrapper* contents,
+ const gfx::Rect& window_bounds,
+ const DockInfo& dock_info,
+ bool maximize) {
+ return NULL;
+ }
+ virtual void ContinueDraggingDetachedTab(TabContentsWrapper* contents,
+ const gfx::Rect& window_bounds,
+ const gfx::Rect& tab_bounds) {
+ }
+ virtual int GetDragActions() const {
+ return 0;
+ }
+ virtual TabContentsWrapper* CreateTabContentsForURL(
+ const GURL& url,
+ const GURL& referrer,
+ Profile* profile,
+ PageTransition::Type transition,
+ bool defer_load,
+ SiteInstance* instance) const {
+ return NULL;
+ }
+ virtual bool CanDuplicateContentsAt(int index) { return true; }
+ virtual void DuplicateContentsAt(int index) { }
+ virtual void CloseFrameAfterDragSession() { }
+ virtual void CreateHistoricalTab(TabContentsWrapper* contents) { }
+ virtual bool RunUnloadListenerBeforeClosing(TabContentsWrapper* contents) {
+ return true;
+ }
+ virtual bool CanRestoreTab() {
+ return true;
+ }
+ virtual void RestoreTab() {}
+
+ virtual bool CanCloseContentsAt(int index) { return true; }
+
+ virtual bool CanBookmarkAllTabs() const { return false; }
+
+ virtual bool CanCloseTab() const { return true; }
+
+ virtual void BookmarkAllTabs() {}
+
+ virtual bool UseVerticalTabs() const { return false; }
+
+ virtual void ToggleUseVerticalTabs() {}
+
+ virtual bool LargeIconsPermitted() const { return true; }
+};
+
+class TabStripControllerTest : public CocoaTest {
+ public:
+ TabStripControllerTest() {
+ Browser* browser = browser_helper_.browser();
+ BrowserWindow* browser_window = browser_helper_.CreateBrowserWindow();
+ NSWindow* window = browser_window->GetNativeHandle();
+ NSView* parent = [window contentView];
+ NSRect content_frame = [parent frame];
+
+ // Create the "switch view" (view that gets changed out when a tab
+ // switches).
+ NSRect switch_frame = NSMakeRect(0, 0, content_frame.size.width, 500);
+ scoped_nsobject<NSView> switch_view(
+ [[NSView alloc] initWithFrame:switch_frame]);
+ [parent addSubview:switch_view.get()];
+
+ // Create the tab strip view. It's expected to have a child button in it
+ // already as the "new tab" button so create that too.
+ NSRect strip_frame = NSMakeRect(0, NSMaxY(switch_frame),
+ content_frame.size.width, 30);
+ scoped_nsobject<TabStripView> tab_strip(
+ [[TabStripView alloc] initWithFrame:strip_frame]);
+ [parent addSubview:tab_strip.get()];
+ NSRect button_frame = NSMakeRect(0, 0, 15, 15);
+ scoped_nsobject<NewTabButton> new_tab_button(
+ [[NewTabButton alloc] initWithFrame:button_frame]);
+ [tab_strip addSubview:new_tab_button.get()];
+ [tab_strip setNewTabButton:new_tab_button.get()];
+
+ delegate_.reset(new TestTabStripDelegate());
+ model_ = browser->tabstrip_model();
+ controller_delegate_.reset([TestTabStripControllerDelegate alloc]);
+ controller_.reset([[TabStripController alloc]
+ initWithView:static_cast<TabStripView*>(tab_strip.get())
+ switchView:switch_view.get()
+ browser:browser
+ delegate:controller_delegate_.get()]);
+ }
+
+ virtual void TearDown() {
+ browser_helper_.CloseBrowserWindow();
+ // The call to CocoaTest::TearDown() deletes the Browser and TabStripModel
+ // objects, so we first have to delete the controller, which refers to them.
+ controller_.reset(nil);
+ model_ = NULL;
+ CocoaTest::TearDown();
+ }
+
+ BrowserTestHelper browser_helper_;
+ scoped_ptr<TestTabStripDelegate> delegate_;
+ TabStripModel* model_;
+ scoped_nsobject<TestTabStripControllerDelegate> controller_delegate_;
+ scoped_nsobject<TabStripController> controller_;
+};
+
+// Test adding and removing tabs and making sure that views get added to
+// the tab strip.
+TEST_F(TabStripControllerTest, AddRemoveTabs) {
+ EXPECT_TRUE(model_->empty());
+ SiteInstance* instance =
+ SiteInstance::CreateSiteInstance(browser_helper_.profile());
+ TabContentsWrapper* tab_contents =
+ Browser::TabContentsFactory(browser_helper_.profile(), instance,
+ MSG_ROUTING_NONE, NULL, NULL);
+ model_->AppendTabContents(tab_contents, true);
+ EXPECT_EQ(model_->count(), 1);
+}
+
+TEST_F(TabStripControllerTest, SelectTab) {
+ // TODO(pinkerton): Implement http://crbug.com/10899
+}
+
+TEST_F(TabStripControllerTest, RearrangeTabs) {
+ // TODO(pinkerton): Implement http://crbug.com/10899
+}
+
+// Test that changing the number of tabs broadcasts a
+// kTabStripNumberOfTabsChanged notifiction.
+TEST_F(TabStripControllerTest, Notifications) {
+ // TODO(pinkerton): Implement http://crbug.com/10899
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/tab_strip_model_observer_bridge.h b/chrome/browser/ui/cocoa/tab_strip_model_observer_bridge.h
new file mode 100644
index 0000000..534fcfb
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_strip_model_observer_bridge.h
@@ -0,0 +1,85 @@
+// 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_UI_COCOA_TAB_STRIP_MODEL_OBSERVER_BRIDGE_H_
+#define CHROME_BROWSER_UI_COCOA_TAB_STRIP_MODEL_OBSERVER_BRIDGE_H_
+#pragma once
+
+#import <Foundation/Foundation.h>
+
+#include "chrome/browser/tabs/tab_strip_model_observer.h"
+
+class TabContentsWrapper;
+class TabStripModel;
+
+// A C++ bridge class to handle receiving notifications from the C++ tab strip
+// model. When the caller allocates a bridge, it automatically registers for
+// notifications from |model| and passes messages to |controller| via the
+// informal protocol below. The owner of this object is responsible for deleting
+// it (and thus unhooking notifications) before |controller| is destroyed.
+class TabStripModelObserverBridge : public TabStripModelObserver {
+ public:
+ TabStripModelObserverBridge(TabStripModel* model, id controller);
+ virtual ~TabStripModelObserverBridge();
+
+ // Overridden from TabStripModelObserver
+ virtual void TabInsertedAt(TabContentsWrapper* contents,
+ int index,
+ bool foreground);
+ virtual void TabClosingAt(TabStripModel* tab_strip_model,
+ TabContentsWrapper* contents,
+ int index);
+ virtual void TabDetachedAt(TabContentsWrapper* contents, int index);
+ virtual void TabSelectedAt(TabContentsWrapper* old_contents,
+ TabContentsWrapper* new_contents,
+ int index,
+ bool user_gesture);
+ virtual void TabMoved(TabContentsWrapper* contents,
+ int from_index,
+ int to_index);
+ virtual void TabChangedAt(TabContentsWrapper* contents, int index,
+ TabChangeType change_type);
+ virtual void TabReplacedAt(TabContentsWrapper* old_contents,
+ TabContentsWrapper* new_contents,
+ int index);
+ virtual void TabMiniStateChanged(TabContentsWrapper* contents, int index);
+ virtual void TabStripEmpty();
+ virtual void TabStripModelDeleted();
+
+ private:
+ id controller_; // weak, owns me
+ TabStripModel* model_; // weak, owned by Browser
+};
+
+// A collection of methods which can be selectively implemented by any
+// Cocoa object to receive updates about changes to a tab strip model. It is
+// ok to not implement them, the calling code checks before calling.
+@interface NSObject(TabStripModelBridge)
+- (void)insertTabWithContents:(TabContentsWrapper*)contents
+ atIndex:(NSInteger)index
+ inForeground:(bool)inForeground;
+- (void)tabClosingWithContents:(TabContentsWrapper*)contents
+ atIndex:(NSInteger)index;
+- (void)tabDetachedWithContents:(TabContentsWrapper*)contents
+ atIndex:(NSInteger)index;
+- (void)selectTabWithContents:(TabContentsWrapper*)newContents
+ previousContents:(TabContentsWrapper*)oldContents
+ atIndex:(NSInteger)index
+ userGesture:(bool)wasUserGesture;
+- (void)tabMovedWithContents:(TabContentsWrapper*)contents
+ fromIndex:(NSInteger)from
+ toIndex:(NSInteger)to;
+- (void)tabChangedWithContents:(TabContentsWrapper*)contents
+ atIndex:(NSInteger)index
+ changeType:(TabStripModelObserver::TabChangeType)change;
+- (void)tabReplacedWithContents:(TabContentsWrapper*)newContents
+ previousContents:(TabContentsWrapper*)oldContents
+ atIndex:(NSInteger)index;
+- (void)tabMiniStateChangedWithContents:(TabContentsWrapper*)contents
+ atIndex:(NSInteger)index;
+- (void)tabStripEmpty;
+- (void)tabStripModelDeleted;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_TAB_STRIP_MODEL_OBSERVER_BRIDGE_H_
diff --git a/chrome/browser/ui/cocoa/tab_strip_model_observer_bridge.mm b/chrome/browser/ui/cocoa/tab_strip_model_observer_bridge.mm
new file mode 100644
index 0000000..a998d23
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_strip_model_observer_bridge.mm
@@ -0,0 +1,118 @@
+// 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 "chrome/browser/ui/cocoa/tab_strip_model_observer_bridge.h"
+
+#include "chrome/browser/tabs/tab_strip_model.h"
+
+TabStripModelObserverBridge::TabStripModelObserverBridge(TabStripModel* model,
+ id controller)
+ : controller_(controller), model_(model) {
+ DCHECK(model && controller);
+ // Register to be a listener on the model so we can get updates and tell
+ // |controller_| about them in the future.
+ model_->AddObserver(this);
+}
+
+TabStripModelObserverBridge::~TabStripModelObserverBridge() {
+ // Remove ourselves from receiving notifications.
+ model_->RemoveObserver(this);
+}
+
+void TabStripModelObserverBridge::TabInsertedAt(TabContentsWrapper* contents,
+ int index,
+ bool foreground) {
+ if ([controller_ respondsToSelector:
+ @selector(insertTabWithContents:atIndex:inForeground:)]) {
+ [controller_ insertTabWithContents:contents
+ atIndex:index
+ inForeground:foreground];
+ }
+}
+
+void TabStripModelObserverBridge::TabClosingAt(TabStripModel* tab_strip_model,
+ TabContentsWrapper* contents,
+ int index) {
+ if ([controller_ respondsToSelector:
+ @selector(tabClosingWithContents:atIndex:)]) {
+ [controller_ tabClosingWithContents:contents atIndex:index];
+ }
+}
+
+void TabStripModelObserverBridge::TabDetachedAt(TabContentsWrapper* contents,
+ int index) {
+ if ([controller_ respondsToSelector:
+ @selector(tabDetachedWithContents:atIndex:)]) {
+ [controller_ tabDetachedWithContents:contents atIndex:index];
+ }
+}
+
+void TabStripModelObserverBridge::TabSelectedAt(
+ TabContentsWrapper* old_contents,
+ TabContentsWrapper* new_contents,
+ int index,
+ bool user_gesture) {
+ if ([controller_ respondsToSelector:
+ @selector(selectTabWithContents:previousContents:atIndex:
+ userGesture:)]) {
+ [controller_ selectTabWithContents:new_contents
+ previousContents:old_contents
+ atIndex:index
+ userGesture:user_gesture];
+ }
+}
+
+void TabStripModelObserverBridge::TabMoved(TabContentsWrapper* contents,
+ int from_index,
+ int to_index) {
+ if ([controller_ respondsToSelector:
+ @selector(tabMovedWithContents:fromIndex:toIndex:)]) {
+ [controller_ tabMovedWithContents:contents
+ fromIndex:from_index
+ toIndex:to_index];
+ }
+}
+
+void TabStripModelObserverBridge::TabChangedAt(TabContentsWrapper* contents,
+ int index,
+ TabChangeType change_type) {
+ if ([controller_ respondsToSelector:
+ @selector(tabChangedWithContents:atIndex:changeType:)]) {
+ [controller_ tabChangedWithContents:contents
+ atIndex:index
+ changeType:change_type];
+ }
+}
+
+void TabStripModelObserverBridge::TabReplacedAt(
+ TabContentsWrapper* old_contents,
+ TabContentsWrapper* new_contents,
+ int index) {
+ if ([controller_ respondsToSelector:
+ @selector(tabReplacedWithContents:previousContents:atIndex:)]) {
+ [controller_ tabReplacedWithContents:new_contents
+ previousContents:old_contents
+ atIndex:index];
+ } else {
+ TabChangedAt(new_contents, index, ALL);
+ }
+}
+
+void TabStripModelObserverBridge::TabMiniStateChanged(
+ TabContentsWrapper* contents, int index) {
+ if ([controller_ respondsToSelector:
+ @selector(tabMiniStateChangedWithContents:atIndex:)]) {
+ [controller_ tabMiniStateChangedWithContents:contents atIndex:index];
+ }
+}
+
+void TabStripModelObserverBridge::TabStripEmpty() {
+ if ([controller_ respondsToSelector:@selector(tabStripEmpty)])
+ [controller_ tabStripEmpty];
+}
+
+void TabStripModelObserverBridge::TabStripModelDeleted() {
+ if ([controller_ respondsToSelector:@selector(tabStripModelDeleted)])
+ [controller_ tabStripModelDeleted];
+}
diff --git a/chrome/browser/ui/cocoa/tab_strip_view.h b/chrome/browser/ui/cocoa/tab_strip_view.h
new file mode 100644
index 0000000..f504ff4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_strip_view.h
@@ -0,0 +1,48 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_TAB_STRIP_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_TAB_STRIP_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/url_drop_target.h"
+
+@class NewTabButton;
+
+// A view class that handles rendering the tab strip and drops of URLS with
+// a positioning locator for drop feedback.
+
+@interface TabStripView : NSView<URLDropTarget> {
+ @private
+ NSTimeInterval lastMouseUp_;
+
+ // Handles being a drag-and-drop target.
+ scoped_nsobject<URLDropTargetHandler> dropHandler_;
+
+ // Weak; the following come from the nib.
+ NewTabButton* newTabButton_;
+
+ // Whether the drop-indicator arrow is shown, and if it is, the coordinate of
+ // its tip.
+ BOOL dropArrowShown_;
+ NSPoint dropArrowPosition_;
+}
+
+@property(assign, nonatomic) IBOutlet NewTabButton* newTabButton;
+@property(assign, nonatomic) BOOL dropArrowShown;
+@property(assign, nonatomic) NSPoint dropArrowPosition;
+
+@end
+
+// Protected methods subclasses can override to alter behavior. Clients should
+// not call these directly.
+@interface TabStripView(Protected)
+- (void)drawBottomBorder:(NSRect)bounds;
+- (BOOL)doubleClickMinimizesWindow;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_TAB_STRIP_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/tab_strip_view.mm b/chrome/browser/ui/cocoa/tab_strip_view.mm
new file mode 100644
index 0000000..2456362
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_strip_view.mm
@@ -0,0 +1,211 @@
+// 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/ui/cocoa/tab_strip_view.h"
+
+#include "base/logging.h"
+#include "base/mac_util.h"
+#include "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/tab_strip_controller.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+
+@implementation TabStripView
+
+@synthesize newTabButton = newTabButton_;
+@synthesize dropArrowShown = dropArrowShown_;
+@synthesize dropArrowPosition = dropArrowPosition_;
+
+- (id)initWithFrame:(NSRect)frame {
+ self = [super initWithFrame:frame];
+ if (self) {
+ // Set lastMouseUp_ = -1000.0 so that timestamp-lastMouseUp_ is big unless
+ // lastMouseUp_ has been reset.
+ lastMouseUp_ = -1000.0;
+
+ // Register to be an URL drop target.
+ dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]);
+ }
+ return self;
+}
+
+// Draw bottom border (a dark border and light highlight). Each tab is
+// responsible for mimicking this bottom border, unless it's the selected
+// tab.
+- (void)drawBorder:(NSRect)bounds {
+ NSRect borderRect, contentRect;
+
+ borderRect = bounds;
+ borderRect.origin.y = 1;
+ borderRect.size.height = 1;
+ [[NSColor colorWithCalibratedWhite:0.0 alpha:0.2] set];
+ NSRectFillUsingOperation(borderRect, NSCompositeSourceOver);
+ NSDivideRect(bounds, &borderRect, &contentRect, 1, NSMinYEdge);
+
+ BrowserThemeProvider* themeProvider =
+ static_cast<BrowserThemeProvider*>([[self window] themeProvider]);
+ if (!themeProvider)
+ return;
+
+ NSColor* bezelColor = themeProvider->GetNSColor(
+ themeProvider->UsingDefaultTheme() ?
+ BrowserThemeProvider::COLOR_TOOLBAR_BEZEL :
+ BrowserThemeProvider::COLOR_TOOLBAR, true);
+ [bezelColor set];
+ NSRectFill(borderRect);
+ NSRectFillUsingOperation(borderRect, NSCompositeSourceOver);
+}
+
+- (void)drawRect:(NSRect)rect {
+ NSRect boundsRect = [self bounds];
+
+ [self drawBorder:boundsRect];
+
+ // Draw drop-indicator arrow (if appropriate).
+ // TODO(viettrungluu): this is all a stop-gap measure.
+ if ([self dropArrowShown]) {
+ // Programmer art: an arrow parametrized by many knobs. Note that the arrow
+ // points downwards (so understand "width" and "height" accordingly).
+
+ // How many (pixels) to inset on the top/bottom.
+ const CGFloat kArrowTopInset = 1.5;
+ const CGFloat kArrowBottomInset = 1;
+
+ // What proportion of the vertical space is dedicated to the arrow tip,
+ // i.e., (arrow tip height)/(amount of vertical space).
+ const CGFloat kArrowTipProportion = 0.5;
+
+ // This is a slope, i.e., (arrow tip height)/(0.5 * arrow tip width).
+ const CGFloat kArrowTipSlope = 1.2;
+
+ // What proportion of the arrow tip width is the stem, i.e., (stem
+ // width)/(arrow tip width).
+ const CGFloat kArrowStemProportion = 0.33;
+
+ NSPoint arrowTipPos = [self dropArrowPosition];
+ arrowTipPos.y += kArrowBottomInset; // Inset on the bottom.
+
+ // Height we have to work with (insetting on the top).
+ CGFloat availableHeight =
+ NSMaxY(boundsRect) - arrowTipPos.y - kArrowTopInset;
+ DCHECK(availableHeight >= 5);
+
+ // Based on the knobs above, calculate actual dimensions which we'll need
+ // for drawing.
+ CGFloat arrowTipHeight = kArrowTipProportion * availableHeight;
+ CGFloat arrowTipWidth = 2 * arrowTipHeight / kArrowTipSlope;
+ CGFloat arrowStemHeight = availableHeight - arrowTipHeight;
+ CGFloat arrowStemWidth = kArrowStemProportion * arrowTipWidth;
+ CGFloat arrowStemInset = (arrowTipWidth - arrowStemWidth) / 2;
+
+ // The line width is arbitrary, but our path really should be mitered.
+ NSBezierPath* arrow = [NSBezierPath bezierPath];
+ [arrow setLineJoinStyle:NSMiterLineJoinStyle];
+ [arrow setLineWidth:1];
+
+ // Define the arrow's shape! We start from the tip and go clockwise.
+ [arrow moveToPoint:arrowTipPos];
+ [arrow relativeLineToPoint:NSMakePoint(-arrowTipWidth / 2, arrowTipHeight)];
+ [arrow relativeLineToPoint:NSMakePoint(arrowStemInset, 0)];
+ [arrow relativeLineToPoint:NSMakePoint(0, arrowStemHeight)];
+ [arrow relativeLineToPoint:NSMakePoint(arrowStemWidth, 0)];
+ [arrow relativeLineToPoint:NSMakePoint(0, -arrowStemHeight)];
+ [arrow relativeLineToPoint:NSMakePoint(arrowStemInset, 0)];
+ [arrow closePath];
+
+ // Draw and fill the arrow.
+ [[NSColor colorWithCalibratedWhite:0 alpha:0.67] set];
+ [arrow stroke];
+ [[NSColor colorWithCalibratedWhite:1 alpha:0.67] setFill];
+ [arrow fill];
+ }
+}
+
+// YES if a double-click in the background of the tab strip minimizes the
+// window.
+- (BOOL)doubleClickMinimizesWindow {
+ return YES;
+}
+
+// We accept first mouse so clicks onto close/zoom/miniaturize buttons and
+// title bar double-clicks are properly detected even when the window is in the
+// background.
+- (BOOL)acceptsFirstMouse:(NSEvent*)event {
+ return YES;
+}
+
+// Trap double-clicks and make them miniaturize the browser window.
+- (void)mouseUp:(NSEvent*)event {
+ // Bail early if double-clicks are disabled.
+ if (![self doubleClickMinimizesWindow]) {
+ [super mouseUp:event];
+ return;
+ }
+
+ NSInteger clickCount = [event clickCount];
+ NSTimeInterval timestamp = [event timestamp];
+
+ // Double-clicks on Zoom/Close/Mininiaturize buttons shouldn't cause
+ // miniaturization. For those, we miss the first click but get the second
+ // (with clickCount == 2!). We thus check that we got a first click shortly
+ // before (measured up-to-up) a double-click. Cocoa doesn't have a documented
+ // way of getting the proper interval (= (double-click-threshold) +
+ // (drag-threshold); the former is Carbon GetDblTime()/60.0 or
+ // com.apple.mouse.doubleClickThreshold [undocumented]). So we hard-code
+ // "short" as 0.8 seconds. (Measuring up-to-up isn't enough to properly
+ // detect double-clicks, but we're actually using Cocoa for that.)
+ if (clickCount == 2 && (timestamp - lastMouseUp_) < 0.8) {
+ if (mac_util::ShouldWindowsMiniaturizeOnDoubleClick())
+ [[self window] performMiniaturize:self];
+ } else {
+ [super mouseUp:event];
+ }
+
+ // If clickCount is 0, the drag threshold was passed.
+ lastMouseUp_ = (clickCount == 1) ? timestamp : -1000.0;
+}
+
+// (URLDropTarget protocol)
+- (id<URLDropTargetController>)urlDropController {
+ BrowserWindowController* windowController = [[self window] windowController];
+ DCHECK([windowController isKindOfClass:[BrowserWindowController class]]);
+ return [windowController tabStripController];
+}
+
+// (URLDropTarget protocol)
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
+ return [dropHandler_ draggingEntered:sender];
+}
+
+// (URLDropTarget protocol)
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
+ return [dropHandler_ draggingUpdated:sender];
+}
+
+// (URLDropTarget protocol)
+- (void)draggingExited:(id<NSDraggingInfo>)sender {
+ return [dropHandler_ draggingExited:sender];
+}
+
+// (URLDropTarget protocol)
+- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
+ return [dropHandler_ performDragOperation:sender];
+}
+
+- (BOOL)accessibilityIsIgnored {
+ return NO;
+}
+
+- (id)accessibilityAttributeValue:(NSString*)attribute {
+ if ([attribute isEqual:NSAccessibilityRoleAttribute])
+ return NSAccessibilityGroupRole;
+
+ return [super accessibilityAttributeValue:attribute];
+}
+
+- (ViewID)viewID {
+ return VIEW_ID_TAB_STRIP;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/tab_strip_view_unittest.mm b/chrome/browser/ui/cocoa/tab_strip_view_unittest.mm
new file mode 100644
index 0000000..2fde99ce
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_strip_view_unittest.mm
@@ -0,0 +1,30 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/tab_strip_view.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class TabStripViewTest : public CocoaTest {
+ public:
+ TabStripViewTest() {
+ NSRect frame = NSMakeRect(0, 0, 100, 30);
+ scoped_nsobject<TabStripView> view(
+ [[TabStripView alloc] initWithFrame:frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ TabStripView* view_;
+};
+
+TEST_VIEW(TabStripViewTest, view_)
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/tab_view.h b/chrome/browser/ui/cocoa/tab_view.h
new file mode 100644
index 0000000..f351650
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_view.h
@@ -0,0 +1,134 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_TAB_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_TAB_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+#include <ApplicationServices/ApplicationServices.h>
+
+#include <map>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/background_gradient_view.h"
+#import "chrome/browser/ui/cocoa/hover_close_button.h"
+
+namespace tabs {
+
+// Nomenclature:
+// Tabs _glow_ under two different circumstances, when they are _hovered_ (by
+// the mouse) and when they are _alerted_ (to show that the tab's title has
+// changed).
+
+// The state of alerting (to show a title change on an unselected, pinned tab).
+// This is more complicated than a simple on/off since we want to allow the
+// alert glow to go through a full rise-hold-fall cycle to avoid flickering (or
+// always holding).
+enum AlertState {
+ kAlertNone = 0, // Obj-C initializes to this.
+ kAlertRising,
+ kAlertHolding,
+ kAlertFalling
+};
+
+} // namespace tabs
+
+@class TabController, TabWindowController;
+
+// A view that handles the event tracking (clicking and dragging) for a tab
+// on the tab strip. Relies on an associated TabController to provide a
+// target/action for selecting the tab.
+
+@interface TabView : BackgroundGradientView {
+ @private
+ IBOutlet TabController* controller_;
+ // TODO(rohitrao): Add this button to a CoreAnimation layer so we can fade it
+ // in and out on mouseovers.
+ IBOutlet HoverCloseButton* closeButton_;
+
+ // See awakeFromNib for purpose.
+ scoped_nsobject<HoverCloseButton> closeButtonRetainer_;
+
+ BOOL closing_;
+
+ // Tracking area for close button mouseover images.
+ scoped_nsobject<NSTrackingArea> closeTrackingArea_;
+
+ BOOL isMouseInside_; // Is the mouse hovering over?
+ tabs::AlertState alertState_;
+
+ CGFloat hoverAlpha_; // How strong the hover glow is.
+ NSTimeInterval hoverHoldEndTime_; // When the hover glow will begin dimming.
+
+ CGFloat alertAlpha_; // How strong the alert glow is.
+ NSTimeInterval alertHoldEndTime_; // When the hover glow will begin dimming.
+
+ NSTimeInterval lastGlowUpdate_; // Time either glow was last updated.
+
+ NSPoint hoverPoint_; // Current location of hover in view coords.
+
+ // All following variables are valid for the duration of a drag.
+ // These are released on mouseUp:
+ BOOL moveWindowOnDrag_; // Set if the only tab of a window is dragged.
+ BOOL tabWasDragged_; // Has the tab been dragged?
+ BOOL draggingWithinTabStrip_; // Did drag stay in the current tab strip?
+ BOOL chromeIsVisible_;
+
+ NSTimeInterval tearTime_; // Time since tear happened
+ NSPoint tearOrigin_; // Origin of the tear rect
+ NSPoint dragOrigin_; // Origin point of the drag
+ // TODO(alcor): these references may need to be strong to avoid crashes
+ // due to JS closing windows
+ TabWindowController* sourceController_; // weak. controller starting the drag
+ NSWindow* sourceWindow_; // weak. The window starting the drag
+ NSRect sourceWindowFrame_;
+ NSRect sourceTabFrame_;
+
+ TabWindowController* draggedController_; // weak. Controller being dragged.
+ NSWindow* dragWindow_; // weak. The window being dragged
+ NSWindow* dragOverlay_; // weak. The overlay being dragged
+ // Cache workspace IDs per-drag because computing them on 10.5 with
+ // CGWindowListCreateDescriptionFromArray is expensive.
+ // resetDragControllers clears this cache.
+ //
+ // TODO(davidben): When 10.5 becomes unsupported, remove this.
+ std::map<CGWindowID, int> workspaceIDCache_;
+
+ TabWindowController* targetController_; // weak. Controller being targeted
+ NSCellStateValue state_;
+}
+
+@property(assign, nonatomic) NSCellStateValue state;
+@property(assign, nonatomic) CGFloat hoverAlpha;
+@property(assign, nonatomic) CGFloat alertAlpha;
+
+// Determines if the tab is in the process of animating closed. It may still
+// be visible on-screen, but should not respond to/initiate any events. Upon
+// setting to NO, clears the target/action of the close button to prevent
+// clicks inside it from sending messages.
+@property(assign, nonatomic, getter=isClosing) BOOL closing;
+
+// Enables/Disables tracking regions for the tab.
+- (void)setTrackingEnabled:(BOOL)enabled;
+
+// Begin showing an "alert" glow (shown to call attention to an unselected
+// pinned tab whose title changed).
+- (void)startAlert;
+
+// Stop showing the "alert" glow; this won't immediately wipe out any glow, but
+// will make it fade away.
+- (void)cancelAlert;
+
+@end
+
+// The TabController |controller_| is not the only owner of this view. If the
+// controller is released before this view, then we could be hanging onto a
+// garbage pointer. To prevent this, the TabController uses this interface to
+// clear the |controller_| pointer when it is dying.
+@interface TabView (TabControllerInterface)
+- (void)setController:(TabController*)controller;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_TAB_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/tab_view.mm b/chrome/browser/ui/cocoa/tab_view.mm
new file mode 100644
index 0000000..58659ca
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_view.mm
@@ -0,0 +1,1057 @@
+// 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/ui/cocoa/tab_view.h"
+
+#include "base/logging.h"
+#import "base/mac_util.h"
+#include "base/mac/scoped_cftyperef.h"
+#include "base/nsimage_cache_mac.h"
+#include "chrome/browser/accessibility/browser_accessibility_state.h"
+#include "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/tab_controller.h"
+#import "chrome/browser/ui/cocoa/tab_window_controller.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+#include "grit/theme_resources.h"
+
+namespace {
+
+// Constants for inset and control points for tab shape.
+const CGFloat kInsetMultiplier = 2.0/3.0;
+const CGFloat kControlPoint1Multiplier = 1.0/3.0;
+const CGFloat kControlPoint2Multiplier = 3.0/8.0;
+
+// The amount of time in seconds during which each type of glow increases, holds
+// steady, and decreases, respectively.
+const NSTimeInterval kHoverShowDuration = 0.2;
+const NSTimeInterval kHoverHoldDuration = 0.02;
+const NSTimeInterval kHoverHideDuration = 0.4;
+const NSTimeInterval kAlertShowDuration = 0.4;
+const NSTimeInterval kAlertHoldDuration = 0.4;
+const NSTimeInterval kAlertHideDuration = 0.4;
+
+// The default time interval in seconds between glow updates (when
+// increasing/decreasing).
+const NSTimeInterval kGlowUpdateInterval = 0.025;
+
+const CGFloat kTearDistance = 36.0;
+const NSTimeInterval kTearDuration = 0.333;
+
+// This is used to judge whether the mouse has moved during rapid closure; if it
+// has moved less than the threshold, we want to close the tab.
+const CGFloat kRapidCloseDist = 2.5;
+
+} // namespace
+
+@interface TabView(Private)
+
+- (void)resetLastGlowUpdateTime;
+- (NSTimeInterval)timeElapsedSinceLastGlowUpdate;
+- (void)adjustGlowValue;
+// TODO(davidben): When we stop supporting 10.5, this can be removed.
+- (int)getWorkspaceID:(NSWindow*)window useCache:(BOOL)useCache;
+- (NSBezierPath*)bezierPathForRect:(NSRect)rect;
+
+@end // TabView(Private)
+
+@implementation TabView
+
+@synthesize state = state_;
+@synthesize hoverAlpha = hoverAlpha_;
+@synthesize alertAlpha = alertAlpha_;
+@synthesize closing = closing_;
+
+- (id)initWithFrame:(NSRect)frame {
+ self = [super initWithFrame:frame];
+ if (self) {
+ [self setShowsDivider:NO];
+ // TODO(alcor): register for theming
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ [self setShowsDivider:NO];
+
+ // It is desirable for us to remove the close button from the cocoa hierarchy,
+ // so that VoiceOver does not encounter it.
+ // TODO(dtseng): crbug.com/59978.
+ // Retain in case we remove it from its superview.
+ closeButtonRetainer_.reset([closeButton_ retain]);
+ if (Singleton<BrowserAccessibilityState>::get()->IsAccessibleBrowser()) {
+ // The superview gives up ownership of the closeButton here.
+ [closeButton_ removeFromSuperview];
+ }
+}
+
+- (void)dealloc {
+ // Cancel any delayed requests that may still be pending (drags or hover).
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+ [super dealloc];
+}
+
+// Called to obtain the context menu for when the user hits the right mouse
+// button (or control-clicks). (Note that -rightMouseDown: is *not* called for
+// control-click.)
+- (NSMenu*)menu {
+ if ([self isClosing])
+ return nil;
+
+ // Sheets, being window-modal, should block contextual menus. For some reason
+ // they do not. Disallow them ourselves.
+ if ([[self window] attachedSheet])
+ return nil;
+
+ return [controller_ menu];
+}
+
+// Overridden so that mouse clicks come to this view (the parent of the
+// hierarchy) first. We want to handle clicks and drags in this class and
+// leave the background button for display purposes only.
+- (BOOL)acceptsFirstMouse:(NSEvent*)theEvent {
+ return YES;
+}
+
+- (void)mouseEntered:(NSEvent*)theEvent {
+ isMouseInside_ = YES;
+ [self resetLastGlowUpdateTime];
+ [self adjustGlowValue];
+}
+
+- (void)mouseMoved:(NSEvent*)theEvent {
+ hoverPoint_ = [self convertPoint:[theEvent locationInWindow]
+ fromView:nil];
+ [self setNeedsDisplay:YES];
+}
+
+- (void)mouseExited:(NSEvent*)theEvent {
+ isMouseInside_ = NO;
+ hoverHoldEndTime_ =
+ [NSDate timeIntervalSinceReferenceDate] + kHoverHoldDuration;
+ [self resetLastGlowUpdateTime];
+ [self adjustGlowValue];
+}
+
+- (void)setTrackingEnabled:(BOOL)enabled {
+ [closeButton_ setTrackingEnabled:enabled];
+}
+
+// Determines which view a click in our frame actually hit. It's either this
+// view or our child close button.
+- (NSView*)hitTest:(NSPoint)aPoint {
+ NSPoint viewPoint = [self convertPoint:aPoint fromView:[self superview]];
+ NSRect frame = [self frame];
+
+ // Reduce the width of the hit rect slightly to remove the overlap
+ // between adjacent tabs. The drawing code in TabCell has the top
+ // corners of the tab inset by height*2/3, so we inset by half of
+ // that here. This doesn't completely eliminate the overlap, but it
+ // works well enough.
+ NSRect hitRect = NSInsetRect(frame, frame.size.height / 3.0f, 0);
+ if (![closeButton_ isHidden])
+ if (NSPointInRect(viewPoint, [closeButton_ frame])) return closeButton_;
+ if (NSPointInRect(aPoint, hitRect)) return self;
+ return nil;
+}
+
+// Returns |YES| if this tab can be torn away into a new window.
+- (BOOL)canBeDragged {
+ if ([self isClosing])
+ return NO;
+ NSWindowController* controller = [sourceWindow_ windowController];
+ if ([controller isKindOfClass:[TabWindowController class]]) {
+ TabWindowController* realController =
+ static_cast<TabWindowController*>(controller);
+ return [realController isTabDraggable:self];
+ }
+ return YES;
+}
+
+// Returns an array of controllers that could be a drop target, ordered front to
+// back. It has to be of the appropriate class, and visible (obviously). Note
+// that the window cannot be a target for itself.
+- (NSArray*)dropTargetsForController:(TabWindowController*)dragController {
+ NSMutableArray* targets = [NSMutableArray array];
+ NSWindow* dragWindow = [dragController window];
+ for (NSWindow* window in [NSApp orderedWindows]) {
+ if (window == dragWindow) continue;
+ if (![window isVisible]) continue;
+ // Skip windows on the wrong space.
+ if ([window respondsToSelector:@selector(isOnActiveSpace)]) {
+ if (![window performSelector:@selector(isOnActiveSpace)])
+ continue;
+ } else {
+ // TODO(davidben): When we stop supporting 10.5, this can be
+ // removed.
+ //
+ // We don't cache the workspace of |dragWindow| because it may
+ // move around spaces.
+ if ([self getWorkspaceID:dragWindow useCache:NO] !=
+ [self getWorkspaceID:window useCache:YES])
+ continue;
+ }
+ NSWindowController* controller = [window windowController];
+ if ([controller isKindOfClass:[TabWindowController class]]) {
+ TabWindowController* realController =
+ static_cast<TabWindowController*>(controller);
+ if ([realController canReceiveFrom:dragController])
+ [targets addObject:controller];
+ }
+ }
+ return targets;
+}
+
+// Call to clear out transient weak references we hold during drags.
+- (void)resetDragControllers {
+ draggedController_ = nil;
+ dragWindow_ = nil;
+ dragOverlay_ = nil;
+ sourceController_ = nil;
+ sourceWindow_ = nil;
+ targetController_ = nil;
+ workspaceIDCache_.clear();
+}
+
+// Sets whether the window background should be visible or invisible when
+// dragging a tab. The background should be invisible when the mouse is over a
+// potential drop target for the tab (the tab strip). It should be visible when
+// there's no drop target so the window looks more fully realized and ready to
+// become a stand-alone window.
+- (void)setWindowBackgroundVisibility:(BOOL)shouldBeVisible {
+ if (chromeIsVisible_ == shouldBeVisible)
+ return;
+
+ // There appears to be a race-condition in CoreAnimation where if we use
+ // animators to set the alpha values, we can't guarantee that we cancel them.
+ // This has the side effect of sometimes leaving the dragged window
+ // translucent or invisible. As a result, don't animate the alpha change.
+ [[draggedController_ overlayWindow] setAlphaValue:1.0];
+ if (targetController_) {
+ [dragWindow_ setAlphaValue:0.0];
+ [[draggedController_ overlayWindow] setHasShadow:YES];
+ [[targetController_ window] makeMainWindow];
+ } else {
+ [dragWindow_ setAlphaValue:0.5];
+ [[draggedController_ overlayWindow] setHasShadow:NO];
+ [[draggedController_ window] makeMainWindow];
+ }
+ chromeIsVisible_ = shouldBeVisible;
+}
+
+// Handle clicks and drags in this button. We get here because we have
+// overridden acceptsFirstMouse: and the click is within our bounds.
+- (void)mouseDown:(NSEvent*)theEvent {
+ if ([self isClosing])
+ return;
+
+ NSPoint downLocation = [theEvent locationInWindow];
+
+ // Record the state of the close button here, because selecting the tab will
+ // unhide it.
+ BOOL closeButtonActive = [closeButton_ isHidden] ? NO : YES;
+
+ // During the tab closure animation (in particular, during rapid tab closure),
+ // we may get incorrectly hit with a mouse down. If it should have gone to the
+ // close button, we send it there -- it should then track the mouse, so we
+ // don't have to worry about mouse ups.
+ if (closeButtonActive && [controller_ inRapidClosureMode]) {
+ NSPoint hitLocation = [[self superview] convertPoint:downLocation
+ fromView:nil];
+ if ([self hitTest:hitLocation] == closeButton_) {
+ [closeButton_ mouseDown:theEvent];
+ return;
+ }
+ }
+
+ // Fire the action to select the tab.
+ if ([[controller_ target] respondsToSelector:[controller_ action]])
+ [[controller_ target] performSelector:[controller_ action]
+ withObject:self];
+
+ [self resetDragControllers];
+
+ // Resolve overlay back to original window.
+ sourceWindow_ = [self window];
+ if ([sourceWindow_ isKindOfClass:[NSPanel class]]) {
+ sourceWindow_ = [sourceWindow_ parentWindow];
+ }
+
+ sourceWindowFrame_ = [sourceWindow_ frame];
+ sourceTabFrame_ = [self frame];
+ sourceController_ = [sourceWindow_ windowController];
+ tabWasDragged_ = NO;
+ tearTime_ = 0.0;
+ draggingWithinTabStrip_ = YES;
+ chromeIsVisible_ = NO;
+
+ // If there's more than one potential window to be a drop target, we want to
+ // treat a drag of a tab just like dragging around a tab that's already
+ // detached. Note that unit tests might have |-numberOfTabs| reporting zero
+ // since the model won't be fully hooked up. We need to be prepared for that
+ // and not send them into the "magnetic" codepath.
+ NSArray* targets = [self dropTargetsForController:sourceController_];
+ moveWindowOnDrag_ =
+ ([sourceController_ numberOfTabs] < 2 && ![targets count]) ||
+ ![self canBeDragged] ||
+ ![sourceController_ tabDraggingAllowed];
+ // If we are dragging a tab, a window with a single tab should immediately
+ // snap off and not drag within the tab strip.
+ if (!moveWindowOnDrag_)
+ draggingWithinTabStrip_ = [sourceController_ numberOfTabs] > 1;
+
+ dragOrigin_ = [NSEvent mouseLocation];
+
+ // If the tab gets torn off, the tab controller will be removed from the tab
+ // strip and then deallocated. This will also result in *us* being
+ // deallocated. Both these are bad, so we prevent this by retaining the
+ // controller.
+ scoped_nsobject<TabController> controller([controller_ retain]);
+
+ // Because we move views between windows, we need to handle the event loop
+ // ourselves. Ideally we should use the standard event loop.
+ while (1) {
+ theEvent =
+ [NSApp nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask
+ untilDate:[NSDate distantFuture]
+ inMode:NSDefaultRunLoopMode dequeue:YES];
+ NSEventType type = [theEvent type];
+ if (type == NSLeftMouseDragged) {
+ [self mouseDragged:theEvent];
+ } else if (type == NSLeftMouseUp) {
+ NSPoint upLocation = [theEvent locationInWindow];
+ CGFloat dx = upLocation.x - downLocation.x;
+ CGFloat dy = upLocation.y - downLocation.y;
+
+ // During rapid tab closure (mashing tab close buttons), we may get hit
+ // with a mouse down. As long as the mouse up is over the close button,
+ // and the mouse hasn't moved too much, we close the tab.
+ if (closeButtonActive &&
+ (dx*dx + dy*dy) <= kRapidCloseDist*kRapidCloseDist &&
+ [controller inRapidClosureMode]) {
+ NSPoint hitLocation =
+ [[self superview] convertPoint:[theEvent locationInWindow]
+ fromView:nil];
+ if ([self hitTest:hitLocation] == closeButton_) {
+ [controller closeTab:self];
+ break;
+ }
+ }
+
+ [self mouseUp:theEvent];
+ break;
+ } else {
+ // TODO(viettrungluu): [crbug.com/23830] We can receive right-mouse-ups
+ // (and maybe even others?) for reasons I don't understand. So we
+ // explicitly check for both events we're expecting, and log others. We
+ // should figure out what's going on.
+ LOG(WARNING) << "Spurious event received of type " << type << ".";
+ }
+ }
+}
+
+- (void)mouseDragged:(NSEvent*)theEvent {
+ // Special-case this to keep the logic below simpler.
+ if (moveWindowOnDrag_) {
+ if ([sourceController_ windowMovementAllowed]) {
+ NSPoint thisPoint = [NSEvent mouseLocation];
+ NSPoint origin = sourceWindowFrame_.origin;
+ origin.x += (thisPoint.x - dragOrigin_.x);
+ origin.y += (thisPoint.y - dragOrigin_.y);
+ [sourceWindow_ setFrameOrigin:NSMakePoint(origin.x, origin.y)];
+ } // else do nothing.
+ return;
+ }
+
+ // First, go through the magnetic drag cycle. We break out of this if
+ // "stretchiness" ever exceeds a set amount.
+ tabWasDragged_ = YES;
+
+ if (draggingWithinTabStrip_) {
+ NSPoint thisPoint = [NSEvent mouseLocation];
+ CGFloat stretchiness = thisPoint.y - dragOrigin_.y;
+ stretchiness = copysign(sqrtf(fabs(stretchiness))/sqrtf(kTearDistance),
+ stretchiness) / 2.0;
+ CGFloat offset = thisPoint.x - dragOrigin_.x;
+ if (fabsf(offset) > 100) stretchiness = 0;
+ [sourceController_ insertPlaceholderForTab:self
+ frame:NSOffsetRect(sourceTabFrame_,
+ offset, 0)
+ yStretchiness:stretchiness];
+ // Check that we haven't pulled the tab too far to start a drag. This
+ // can include either pulling it too far down, or off the side of the tab
+ // strip that would cause it to no longer be fully visible.
+ BOOL stillVisible = [sourceController_ isTabFullyVisible:self];
+ CGFloat tearForce = fabs(thisPoint.y - dragOrigin_.y);
+ if ([sourceController_ tabTearingAllowed] &&
+ (tearForce > kTearDistance || !stillVisible)) {
+ draggingWithinTabStrip_ = NO;
+ // When you finally leave the strip, we treat that as the origin.
+ dragOrigin_.x = thisPoint.x;
+ } else {
+ // Still dragging within the tab strip, wait for the next drag event.
+ return;
+ }
+ }
+
+ // Do not start dragging until the user has "torn" the tab off by
+ // moving more than 3 pixels.
+ NSDate* targetDwellDate = nil; // The date this target was first chosen.
+
+ NSPoint thisPoint = [NSEvent mouseLocation];
+
+ // Iterate over possible targets checking for the one the mouse is in.
+ // If the tab is just in the frame, bring the window forward to make it
+ // easier to drop something there. If it's in the tab strip, set the new
+ // target so that it pops into that window. We can't cache this because we
+ // need the z-order to be correct.
+ NSArray* targets = [self dropTargetsForController:draggedController_];
+ TabWindowController* newTarget = nil;
+ for (TabWindowController* target in targets) {
+ NSRect windowFrame = [[target window] frame];
+ if (NSPointInRect(thisPoint, windowFrame)) {
+ [[target window] orderFront:self];
+ NSRect tabStripFrame = [[target tabStripView] frame];
+ tabStripFrame.origin = [[target window]
+ convertBaseToScreen:tabStripFrame.origin];
+ if (NSPointInRect(thisPoint, tabStripFrame)) {
+ newTarget = target;
+ }
+ break;
+ }
+ }
+
+ // If we're now targeting a new window, re-layout the tabs in the old
+ // target and reset how long we've been hovering over this new one.
+ if (targetController_ != newTarget) {
+ targetDwellDate = [NSDate date];
+ [targetController_ removePlaceholder];
+ targetController_ = newTarget;
+ if (!newTarget) {
+ tearTime_ = [NSDate timeIntervalSinceReferenceDate];
+ tearOrigin_ = [dragWindow_ frame].origin;
+ }
+ }
+
+ // Create or identify the dragged controller.
+ if (!draggedController_) {
+ // Get rid of any placeholder remaining in the original source window.
+ [sourceController_ removePlaceholder];
+
+ // Detach from the current window and put it in a new window. If there are
+ // no more tabs remaining after detaching, the source window is about to
+ // go away (it's been autoreleased) so we need to ensure we don't reference
+ // it any more. In that case the new controller becomes our source
+ // controller.
+ draggedController_ = [sourceController_ detachTabToNewWindow:self];
+ dragWindow_ = [draggedController_ window];
+ [dragWindow_ setAlphaValue:0.0];
+ if (![sourceController_ hasLiveTabs]) {
+ sourceController_ = draggedController_;
+ sourceWindow_ = dragWindow_;
+ }
+
+ // If dragging the tab only moves the current window, do not show overlay
+ // so that sheets stay on top of the window.
+ // Bring the target window to the front and make sure it has a border.
+ [dragWindow_ setLevel:NSFloatingWindowLevel];
+ [dragWindow_ setHasShadow:YES];
+ [dragWindow_ orderFront:nil];
+ [dragWindow_ makeMainWindow];
+ [draggedController_ showOverlay];
+ dragOverlay_ = [draggedController_ overlayWindow];
+ // Force the new tab button to be hidden. We'll reset it on mouse up.
+ [draggedController_ showNewTabButton:NO];
+ tearTime_ = [NSDate timeIntervalSinceReferenceDate];
+ tearOrigin_ = sourceWindowFrame_.origin;
+ }
+
+ // TODO(pinkerton): http://crbug.com/25682 demonstrates a way to get here by
+ // some weird circumstance that doesn't first go through mouseDown:. We
+ // really shouldn't go any farther.
+ if (!draggedController_ || !sourceController_)
+ return;
+
+ // When the user first tears off the window, we want slide the window to
+ // the current mouse location (to reduce the jarring appearance). We do this
+ // by calling ourselves back with additional mouseDragged calls (not actual
+ // events). |tearProgress| is a normalized measure of how far through this
+ // tear "animation" (of length kTearDuration) we are and has values [0..1].
+ // We use sqrt() so the animation is non-linear (slow down near the end
+ // point).
+ NSTimeInterval tearProgress =
+ [NSDate timeIntervalSinceReferenceDate] - tearTime_;
+ tearProgress /= kTearDuration; // Normalize.
+ tearProgress = sqrtf(MAX(MIN(tearProgress, 1.0), 0.0));
+
+ // Move the dragged window to the right place on the screen.
+ NSPoint origin = sourceWindowFrame_.origin;
+ origin.x += (thisPoint.x - dragOrigin_.x);
+ origin.y += (thisPoint.y - dragOrigin_.y);
+
+ if (tearProgress < 1) {
+ // If the tear animation is not complete, call back to ourself with the
+ // same event to animate even if the mouse isn't moving. We need to make
+ // sure these get cancelled in mouseUp:.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+ [self performSelector:@selector(mouseDragged:)
+ withObject:theEvent
+ afterDelay:1.0f/30.0f];
+
+ // Set the current window origin based on how far we've progressed through
+ // the tear animation.
+ origin.x = (1 - tearProgress) * tearOrigin_.x + tearProgress * origin.x;
+ origin.y = (1 - tearProgress) * tearOrigin_.y + tearProgress * origin.y;
+ }
+
+ if (targetController_) {
+ // In order to "snap" two windows of different sizes together at their
+ // toolbar, we can't just use the origin of the target frame. We also have
+ // to take into consideration the difference in height.
+ NSRect targetFrame = [[targetController_ window] frame];
+ NSRect sourceFrame = [dragWindow_ frame];
+ origin.y = NSMinY(targetFrame) +
+ (NSHeight(targetFrame) - NSHeight(sourceFrame));
+ }
+ [dragWindow_ setFrameOrigin:NSMakePoint(origin.x, origin.y)];
+
+ // If we're not hovering over any window, make the window fully
+ // opaque. Otherwise, find where the tab might be dropped and insert
+ // a placeholder so it appears like it's part of that window.
+ if (targetController_) {
+ if (![[targetController_ window] isKeyWindow]) {
+ // && ([targetDwellDate timeIntervalSinceNow] < -REQUIRED_DWELL)) {
+ [[targetController_ window] orderFront:nil];
+ targetDwellDate = nil;
+ }
+
+ // Compute where placeholder should go and insert it into the
+ // destination tab strip.
+ TabView* draggedTabView = (TabView*)[draggedController_ selectedTabView];
+ NSRect tabFrame = [draggedTabView frame];
+ tabFrame.origin = [dragWindow_ convertBaseToScreen:tabFrame.origin];
+ tabFrame.origin = [[targetController_ window]
+ convertScreenToBase:tabFrame.origin];
+ tabFrame = [[targetController_ tabStripView]
+ convertRect:tabFrame fromView:nil];
+ [targetController_ insertPlaceholderForTab:self
+ frame:tabFrame
+ yStretchiness:0];
+ [targetController_ layoutTabs];
+ } else {
+ [dragWindow_ makeKeyAndOrderFront:nil];
+ }
+
+ // Adjust the visibility of the window background. If there is a drop target,
+ // we want to hide the window background so the tab stands out for
+ // positioning. If not, we want to show it so it looks like a new window will
+ // be realized.
+ BOOL chromeShouldBeVisible = targetController_ == nil;
+ [self setWindowBackgroundVisibility:chromeShouldBeVisible];
+}
+
+- (void)mouseUp:(NSEvent*)theEvent {
+ // The drag/click is done. If the user dragged the mouse, finalize the drag
+ // and clean up.
+
+ // Special-case this to keep the logic below simpler.
+ if (moveWindowOnDrag_)
+ return;
+
+ // Cancel any delayed -mouseDragged: requests that may still be pending.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+
+ // TODO(pinkerton): http://crbug.com/25682 demonstrates a way to get here by
+ // some weird circumstance that doesn't first go through mouseDown:. We
+ // really shouldn't go any farther.
+ if (!sourceController_)
+ return;
+
+ // We are now free to re-display the new tab button in the window we're
+ // dragging. It will show when the next call to -layoutTabs (which happens
+ // indrectly by several of the calls below, such as removing the placeholder).
+ [draggedController_ showNewTabButton:YES];
+
+ if (draggingWithinTabStrip_) {
+ if (tabWasDragged_) {
+ // Move tab to new location.
+ DCHECK([sourceController_ numberOfTabs]);
+ TabWindowController* dropController = sourceController_;
+ [dropController moveTabView:[dropController selectedTabView]
+ fromController:nil];
+ }
+ } else if (targetController_) {
+ // Move between windows. If |targetController_| is nil, we're not dropping
+ // into any existing window.
+ NSView* draggedTabView = [draggedController_ selectedTabView];
+ [targetController_ moveTabView:draggedTabView
+ fromController:draggedController_];
+ // Force redraw to avoid flashes of old content before returning to event
+ // loop.
+ [[targetController_ window] display];
+ [targetController_ showWindow:nil];
+ [draggedController_ removeOverlay];
+ } else {
+ // Only move the window around on screen. Make sure it's set back to
+ // normal state (fully opaque, has shadow, has key, etc).
+ [draggedController_ removeOverlay];
+ // Don't want to re-show the window if it was closed during the drag.
+ if ([dragWindow_ isVisible]) {
+ [dragWindow_ setAlphaValue:1.0];
+ [dragOverlay_ setHasShadow:NO];
+ [dragWindow_ setHasShadow:YES];
+ [dragWindow_ makeKeyAndOrderFront:nil];
+ }
+ [[draggedController_ window] setLevel:NSNormalWindowLevel];
+ [draggedController_ removePlaceholder];
+ }
+ [sourceController_ removePlaceholder];
+ chromeIsVisible_ = YES;
+
+ [self resetDragControllers];
+}
+
+- (void)otherMouseUp:(NSEvent*)theEvent {
+ if ([self isClosing])
+ return;
+
+ // Support middle-click-to-close.
+ if ([theEvent buttonNumber] == 2) {
+ // |-hitTest:| takes a location in the superview's coordinates.
+ NSPoint upLocation =
+ [[self superview] convertPoint:[theEvent locationInWindow]
+ fromView:nil];
+ // If the mouse up occurred in our view or over the close button, then
+ // close.
+ if ([self hitTest:upLocation])
+ [controller_ closeTab:self];
+ }
+}
+
+- (void)drawRect:(NSRect)dirtyRect {
+ NSGraphicsContext* context = [NSGraphicsContext currentContext];
+ [context saveGraphicsState];
+
+ BrowserThemeProvider* themeProvider =
+ static_cast<BrowserThemeProvider*>([[self window] themeProvider]);
+ [context setPatternPhase:[[self window] themePatternPhase]];
+
+ NSRect rect = [self bounds];
+ NSBezierPath* path = [self bezierPathForRect:rect];
+
+ BOOL selected = [self state];
+ // Don't draw the window/tab bar background when selected, since the tab
+ // background overlay drawn over it (see below) will be fully opaque.
+ BOOL hasBackgroundImage = NO;
+ if (!selected) {
+ // ThemeProvider::HasCustomImage is true only if the theme provides the
+ // image. However, even if the theme doesn't provide a tab background, the
+ // theme machinery will make one if given a frame image. See
+ // BrowserThemePack::GenerateTabBackgroundImages for details.
+ hasBackgroundImage = themeProvider &&
+ (themeProvider->HasCustomImage(IDR_THEME_TAB_BACKGROUND) ||
+ themeProvider->HasCustomImage(IDR_THEME_FRAME));
+
+ NSColor* backgroundImageColor = hasBackgroundImage ?
+ themeProvider->GetNSImageColorNamed(IDR_THEME_TAB_BACKGROUND, true) :
+ nil;
+
+ if (backgroundImageColor) {
+ [backgroundImageColor set];
+ [path fill];
+ } else {
+ // Use the window's background color rather than |[NSColor
+ // windowBackgroundColor]|, which gets confused by the fullscreen window.
+ // (The result is the same for normal, non-fullscreen windows.)
+ [[[self window] backgroundColor] set];
+ [path fill];
+ [[NSColor colorWithCalibratedWhite:1.0 alpha:0.3] set];
+ [path fill];
+ }
+ }
+
+ [context saveGraphicsState];
+ [path addClip];
+
+ // Use the same overlay for the selected state and for hover and alert glows;
+ // for the selected state, it's fully opaque.
+ CGFloat hoverAlpha = [self hoverAlpha];
+ CGFloat alertAlpha = [self alertAlpha];
+ if (selected || hoverAlpha > 0 || alertAlpha > 0) {
+ // Draw the selected background / glow overlay.
+ [context saveGraphicsState];
+ CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
+ CGContextBeginTransparencyLayer(cgContext, 0);
+ if (!selected) {
+ // The alert glow overlay is like the selected state but at most at most
+ // 80% opaque. The hover glow brings up the overlay's opacity at most 50%.
+ CGFloat backgroundAlpha = 0.8 * alertAlpha;
+ backgroundAlpha += (1 - backgroundAlpha) * 0.5 * hoverAlpha;
+ CGContextSetAlpha(cgContext, backgroundAlpha);
+ }
+ [path addClip];
+ [context saveGraphicsState];
+ [super drawBackground];
+ [context restoreGraphicsState];
+
+ // Draw a mouse hover gradient for the default themes.
+ if (!selected && hoverAlpha > 0) {
+ if (themeProvider && !hasBackgroundImage) {
+ scoped_nsobject<NSGradient> glow([NSGradient alloc]);
+ [glow initWithStartingColor:[NSColor colorWithCalibratedWhite:1.0
+ alpha:1.0 * hoverAlpha]
+ endingColor:[NSColor colorWithCalibratedWhite:1.0
+ alpha:0.0]];
+
+ NSPoint point = hoverPoint_;
+ point.y = NSHeight(rect);
+ [glow drawFromCenter:point
+ radius:0.0
+ toCenter:point
+ radius:NSWidth(rect) / 3.0
+ options:NSGradientDrawsBeforeStartingLocation];
+
+ [glow drawInBezierPath:path relativeCenterPosition:hoverPoint_];
+ }
+ }
+
+ CGContextEndTransparencyLayer(cgContext);
+ [context restoreGraphicsState];
+ }
+
+ BOOL active = [[self window] isKeyWindow] || [[self window] isMainWindow];
+ CGFloat borderAlpha = selected ? (active ? 0.3 : 0.2) : 0.2;
+ NSColor* borderColor = [NSColor colorWithDeviceWhite:0.0 alpha:borderAlpha];
+ NSColor* highlightColor = themeProvider ? themeProvider->GetNSColor(
+ themeProvider->UsingDefaultTheme() ?
+ BrowserThemeProvider::COLOR_TOOLBAR_BEZEL :
+ BrowserThemeProvider::COLOR_TOOLBAR, true) : nil;
+
+ // Draw the top inner highlight within the currently selected tab if using
+ // the default theme.
+ if (selected && themeProvider && themeProvider->UsingDefaultTheme()) {
+ NSAffineTransform* highlightTransform = [NSAffineTransform transform];
+ [highlightTransform translateXBy:1.0 yBy:-1.0];
+ scoped_nsobject<NSBezierPath> highlightPath([path copy]);
+ [highlightPath transformUsingAffineTransform:highlightTransform];
+ [highlightColor setStroke];
+ [highlightPath setLineWidth:1.0];
+ [highlightPath stroke];
+ highlightTransform = [NSAffineTransform transform];
+ [highlightTransform translateXBy:-2.0 yBy:0.0];
+ [highlightPath transformUsingAffineTransform:highlightTransform];
+ [highlightPath stroke];
+ }
+
+ [context restoreGraphicsState];
+
+ // Draw the top stroke.
+ [context saveGraphicsState];
+ [borderColor set];
+ [path setLineWidth:1.0];
+ [path stroke];
+ [context restoreGraphicsState];
+
+ // Mimic the tab strip's bottom border, which consists of a dark border
+ // and light highlight.
+ if (!selected) {
+ [path addClip];
+ NSRect borderRect = rect;
+ borderRect.origin.y = 1;
+ borderRect.size.height = 1;
+ [borderColor set];
+ NSRectFillUsingOperation(borderRect, NSCompositeSourceOver);
+
+ borderRect.origin.y = 0;
+ [highlightColor set];
+ NSRectFillUsingOperation(borderRect, NSCompositeSourceOver);
+ }
+
+ [context restoreGraphicsState];
+}
+
+- (void)viewDidMoveToWindow {
+ [super viewDidMoveToWindow];
+ if ([self window]) {
+ [controller_ updateTitleColor];
+ }
+}
+
+- (void)setClosing:(BOOL)closing {
+ closing_ = closing; // Safe because the property is nonatomic.
+ // When closing, ensure clicks to the close button go nowhere.
+ if (closing) {
+ [closeButton_ setTarget:nil];
+ [closeButton_ setAction:nil];
+ }
+}
+
+- (void)startAlert {
+ // Do not start a new alert while already alerting or while in a decay cycle.
+ if (alertState_ == tabs::kAlertNone) {
+ alertState_ = tabs::kAlertRising;
+ [self resetLastGlowUpdateTime];
+ [self adjustGlowValue];
+ }
+}
+
+- (void)cancelAlert {
+ if (alertState_ != tabs::kAlertNone) {
+ alertState_ = tabs::kAlertFalling;
+ alertHoldEndTime_ =
+ [NSDate timeIntervalSinceReferenceDate] + kGlowUpdateInterval;
+ [self resetLastGlowUpdateTime];
+ [self adjustGlowValue];
+ }
+}
+
+- (BOOL)accessibilityIsIgnored {
+ return NO;
+}
+
+- (NSArray*)accessibilityActionNames {
+ NSArray* parentActions = [super accessibilityActionNames];
+
+ return [parentActions arrayByAddingObject:NSAccessibilityPressAction];
+}
+
+- (NSArray*)accessibilityAttributeNames {
+ NSMutableArray* attributes =
+ [[super accessibilityAttributeNames] mutableCopy];
+ [attributes addObject:NSAccessibilityTitleAttribute];
+ [attributes addObject:NSAccessibilityEnabledAttribute];
+
+ return attributes;
+}
+
+- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
+ if ([attribute isEqual:NSAccessibilityTitleAttribute])
+ return NO;
+
+ if ([attribute isEqual:NSAccessibilityEnabledAttribute])
+ return NO;
+
+ return [super accessibilityIsAttributeSettable:attribute];
+}
+
+- (id)accessibilityAttributeValue:(NSString*)attribute {
+ if ([attribute isEqual:NSAccessibilityRoleAttribute])
+ return NSAccessibilityButtonRole;
+
+ if ([attribute isEqual:NSAccessibilityTitleAttribute])
+ return [controller_ title];
+
+ if ([attribute isEqual:NSAccessibilityEnabledAttribute])
+ return [NSNumber numberWithBool:YES];
+
+ if ([attribute isEqual:NSAccessibilityChildrenAttribute]) {
+ // The subviews (icon and text) are clutter; filter out everything but
+ // useful controls.
+ NSArray* children = [super accessibilityAttributeValue:attribute];
+ NSMutableArray* okChildren = [NSMutableArray array];
+ for (id child in children) {
+ if ([child isKindOfClass:[NSButtonCell class]])
+ [okChildren addObject:child];
+ }
+
+ return okChildren;
+ }
+
+ return [super accessibilityAttributeValue:attribute];
+}
+
+- (ViewID)viewID {
+ return VIEW_ID_TAB;
+}
+
+@end // @implementation TabView
+
+@implementation TabView (TabControllerInterface)
+
+- (void)setController:(TabController*)controller {
+ controller_ = controller;
+}
+
+@end // @implementation TabView (TabControllerInterface)
+
+@implementation TabView(Private)
+
+- (void)resetLastGlowUpdateTime {
+ lastGlowUpdate_ = [NSDate timeIntervalSinceReferenceDate];
+}
+
+- (NSTimeInterval)timeElapsedSinceLastGlowUpdate {
+ return [NSDate timeIntervalSinceReferenceDate] - lastGlowUpdate_;
+}
+
+- (void)adjustGlowValue {
+ // A time interval long enough to represent no update.
+ const NSTimeInterval kNoUpdate = 1000000;
+
+ // Time until next update for either glow.
+ NSTimeInterval nextUpdate = kNoUpdate;
+
+ NSTimeInterval elapsed = [self timeElapsedSinceLastGlowUpdate];
+ NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
+
+ // TODO(viettrungluu): <http://crbug.com/30617> -- split off the stuff below
+ // into a pure function and add a unit test.
+
+ CGFloat hoverAlpha = [self hoverAlpha];
+ if (isMouseInside_) {
+ // Increase hover glow until it's 1.
+ if (hoverAlpha < 1) {
+ hoverAlpha = MIN(hoverAlpha + elapsed / kHoverShowDuration, 1);
+ [self setHoverAlpha:hoverAlpha];
+ nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
+ } // Else already 1 (no update needed).
+ } else {
+ if (currentTime >= hoverHoldEndTime_) {
+ // No longer holding, so decrease hover glow until it's 0.
+ if (hoverAlpha > 0) {
+ hoverAlpha = MAX(hoverAlpha - elapsed / kHoverHideDuration, 0);
+ [self setHoverAlpha:hoverAlpha];
+ nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
+ } // Else already 0 (no update needed).
+ } else {
+ // Schedule update for end of hold time.
+ nextUpdate = MIN(hoverHoldEndTime_ - currentTime, nextUpdate);
+ }
+ }
+
+ CGFloat alertAlpha = [self alertAlpha];
+ if (alertState_ == tabs::kAlertRising) {
+ // Increase alert glow until it's 1 ...
+ alertAlpha = MIN(alertAlpha + elapsed / kAlertShowDuration, 1);
+ [self setAlertAlpha:alertAlpha];
+
+ // ... and having reached 1, switch to holding.
+ if (alertAlpha >= 1) {
+ alertState_ = tabs::kAlertHolding;
+ alertHoldEndTime_ = currentTime + kAlertHoldDuration;
+ nextUpdate = MIN(kAlertHoldDuration, nextUpdate);
+ } else {
+ nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
+ }
+ } else if (alertState_ != tabs::kAlertNone) {
+ if (alertAlpha > 0) {
+ if (currentTime >= alertHoldEndTime_) {
+ // Stop holding, then decrease alert glow (until it's 0).
+ if (alertState_ == tabs::kAlertHolding) {
+ alertState_ = tabs::kAlertFalling;
+ nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
+ } else {
+ DCHECK_EQ(tabs::kAlertFalling, alertState_);
+ alertAlpha = MAX(alertAlpha - elapsed / kAlertHideDuration, 0);
+ [self setAlertAlpha:alertAlpha];
+ nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
+ }
+ } else {
+ // Schedule update for end of hold time.
+ nextUpdate = MIN(alertHoldEndTime_ - currentTime, nextUpdate);
+ }
+ } else {
+ // Done the alert decay cycle.
+ alertState_ = tabs::kAlertNone;
+ }
+ }
+
+ if (nextUpdate < kNoUpdate)
+ [self performSelector:_cmd withObject:nil afterDelay:nextUpdate];
+
+ [self resetLastGlowUpdateTime];
+ [self setNeedsDisplay:YES];
+}
+
+// Returns the workspace id of |window|. If |useCache|, then lookup
+// and remember the value in |workspaceIDCache_| until the end of the
+// current drag.
+- (int)getWorkspaceID:(NSWindow*)window useCache:(BOOL)useCache {
+ CGWindowID windowID = [window windowNumber];
+ if (useCache) {
+ std::map<CGWindowID, int>::iterator iter =
+ workspaceIDCache_.find(windowID);
+ if (iter != workspaceIDCache_.end())
+ return iter->second;
+ }
+
+ int workspace = -1;
+ // It's possible to query in bulk, but probably not necessary.
+ base::mac::ScopedCFTypeRef<CFArrayRef> windowIDs(CFArrayCreate(
+ NULL, reinterpret_cast<const void **>(&windowID), 1, NULL));
+ base::mac::ScopedCFTypeRef<CFArrayRef> descriptions(
+ CGWindowListCreateDescriptionFromArray(windowIDs));
+ DCHECK(CFArrayGetCount(descriptions.get()) <= 1);
+ if (CFArrayGetCount(descriptions.get()) > 0) {
+ CFDictionaryRef dict = static_cast<CFDictionaryRef>(
+ CFArrayGetValueAtIndex(descriptions.get(), 0));
+ DCHECK(CFGetTypeID(dict) == CFDictionaryGetTypeID());
+
+ // Sanity check the ID.
+ CFNumberRef otherIDRef = (CFNumberRef)mac_util::GetValueFromDictionary(
+ dict, kCGWindowNumber, CFNumberGetTypeID());
+ CGWindowID otherID;
+ if (otherIDRef &&
+ CFNumberGetValue(otherIDRef, kCGWindowIDCFNumberType, &otherID) &&
+ otherID == windowID) {
+ // And then get the workspace.
+ CFNumberRef workspaceRef = (CFNumberRef)mac_util::GetValueFromDictionary(
+ dict, kCGWindowWorkspace, CFNumberGetTypeID());
+ if (!workspaceRef ||
+ !CFNumberGetValue(workspaceRef, kCFNumberIntType, &workspace)) {
+ workspace = -1;
+ }
+ } else {
+ NOTREACHED();
+ }
+ }
+ if (useCache) {
+ workspaceIDCache_[windowID] = workspace;
+ }
+ return workspace;
+}
+
+// Returns the bezier path used to draw the tab given the bounds to draw it in.
+- (NSBezierPath*)bezierPathForRect:(NSRect)rect {
+ // Outset by 0.5 in order to draw on pixels rather than on borders (which
+ // would cause blurry pixels). Subtract 1px of height to compensate, otherwise
+ // clipping will occur.
+ rect = NSInsetRect(rect, -0.5, -0.5);
+ rect.size.height -= 1.0;
+
+ NSPoint bottomLeft = NSMakePoint(NSMinX(rect), NSMinY(rect) + 2);
+ NSPoint bottomRight = NSMakePoint(NSMaxX(rect), NSMinY(rect) + 2);
+ NSPoint topRight =
+ NSMakePoint(NSMaxX(rect) - kInsetMultiplier * NSHeight(rect),
+ NSMaxY(rect));
+ NSPoint topLeft =
+ NSMakePoint(NSMinX(rect) + kInsetMultiplier * NSHeight(rect),
+ NSMaxY(rect));
+
+ CGFloat baseControlPointOutset = NSHeight(rect) * kControlPoint1Multiplier;
+ CGFloat bottomControlPointInset = NSHeight(rect) * kControlPoint2Multiplier;
+
+ // Outset many of these values by 1 to cause the fill to bleed outside the
+ // clip area.
+ NSBezierPath* path = [NSBezierPath bezierPath];
+ [path moveToPoint:NSMakePoint(bottomLeft.x - 1, bottomLeft.y - 2)];
+ [path lineToPoint:NSMakePoint(bottomLeft.x - 1, bottomLeft.y)];
+ [path lineToPoint:bottomLeft];
+ [path curveToPoint:topLeft
+ controlPoint1:NSMakePoint(bottomLeft.x + baseControlPointOutset,
+ bottomLeft.y)
+ controlPoint2:NSMakePoint(topLeft.x - bottomControlPointInset,
+ topLeft.y)];
+ [path lineToPoint:topRight];
+ [path curveToPoint:bottomRight
+ controlPoint1:NSMakePoint(topRight.x + bottomControlPointInset,
+ topRight.y)
+ controlPoint2:NSMakePoint(bottomRight.x - baseControlPointOutset,
+ bottomRight.y)];
+ [path lineToPoint:NSMakePoint(bottomRight.x + 1, bottomRight.y)];
+ [path lineToPoint:NSMakePoint(bottomRight.x + 1, bottomRight.y - 2)];
+ return path;
+}
+
+@end // @implementation TabView(Private)
diff --git a/chrome/browser/ui/cocoa/tab_view_picker_table.h b/chrome/browser/ui/cocoa/tab_view_picker_table.h
new file mode 100644
index 0000000..75d943f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_view_picker_table.h
@@ -0,0 +1,29 @@
+// 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 "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+
+// TabViewPickerTable is an NSOutlineView that can be used to switch between the
+// NSTabViewItems of an NSTabView. To use this, just create a
+// TabViewPickerTable in Interface Builder and connect the |tabView_| outlet
+// to an NSTabView. Now the table is automatically populated with the tab labels
+// of the tab view, clicking the table updates the tab view, and switching
+// tab view items updates the selection of the table.
+@interface TabViewPickerTable : NSOutlineView <NSTabViewDelegate,
+ NSOutlineViewDelegate,
+ NSOutlineViewDataSource> {
+ @public
+ IBOutlet NSTabView* tabView_; // Visible for testing.
+
+ @private
+ id oldTabViewDelegate_;
+
+ // Shown above all the tab names. May be |nil|.
+ scoped_nsobject<NSString> heading_;
+}
+@property (nonatomic, copy) NSString* heading;
+@end
diff --git a/chrome/browser/ui/cocoa/tab_view_picker_table.mm b/chrome/browser/ui/cocoa/tab_view_picker_table.mm
new file mode 100644
index 0000000..95dec3b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_view_picker_table.mm
@@ -0,0 +1,193 @@
+// 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 "tab_view_picker_table.h"
+
+#include "base/logging.h"
+
+@interface TabViewPickerTable (Private)
+// If a heading is shown, the indices between the tab items and the table rows
+// are shifted by one. These functions convert between tab indices and table
+// indices.
+- (NSInteger)tabIndexFromTableIndex:(NSInteger)tableIndex;
+- (NSInteger)tableIndexFromTabIndex:(NSInteger)tabIndex;
+
+// Returns if |item| is the item shown as heading. If |heading_| is nil, this
+// always returns |NO|.
+- (BOOL)isHeadingItem:(id)item;
+
+// Reloads the outline view and sets the selection to the row corresponding to
+// the currently selected tab.
+- (void)reloadDataWhileKeepingCurrentTabSelected;
+@end
+
+@implementation TabViewPickerTable
+
+- (id)initWithFrame:(NSRect)frame {
+ if ((self = [super initWithFrame:frame])) {
+ [self setDelegate:self];
+ [self setDataSource:self];
+ }
+ return self;
+}
+
+- (id)initWithCoder:(NSCoder*)coder {
+ if ((self = [super initWithCoder:coder])) {
+ [self setDelegate:self];
+ [self setDataSource:self];
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ DCHECK(tabView_);
+ DCHECK_EQ([self delegate], self);
+ DCHECK_EQ([self dataSource], self);
+ DCHECK(![self allowsEmptySelection]);
+ DCHECK(![self allowsMultipleSelection]);
+
+ // Suppress the "Selection changed" message that's sent while the table is
+ // being built for the first time (this causes a selection change to index 0
+ // and back to the prior index).
+ id oldTabViewDelegate = [tabView_ delegate];
+ [tabView_ setDelegate:nil];
+
+ [self reloadDataWhileKeepingCurrentTabSelected];
+
+ oldTabViewDelegate_ = oldTabViewDelegate;
+ [tabView_ setDelegate:self];
+}
+
+- (NSString*)heading {
+ return heading_.get();
+}
+
+- (void)setHeading:(NSString*)str {
+ heading_.reset([str copy]);
+ [self reloadDataWhileKeepingCurrentTabSelected];
+}
+
+- (void)reloadDataWhileKeepingCurrentTabSelected {
+ NSInteger index =
+ [tabView_ indexOfTabViewItem:[tabView_ selectedTabViewItem]];
+ [self reloadData];
+ if (heading_)
+ [self expandItem:[self outlineView:self child:0 ofItem:nil]];
+ NSIndexSet* indexSet =
+ [NSIndexSet indexSetWithIndex:[self tableIndexFromTabIndex:index]];
+ [self selectRowIndexes:indexSet byExtendingSelection:NO];
+}
+
+// NSTabViewDelegate methods.
+- (void) tabView:(NSTabView*)tabView
+ didSelectTabViewItem:(NSTabViewItem*)tabViewItem {
+ DCHECK_EQ(tabView_, tabView);
+ NSInteger index =
+ [tabView_ indexOfTabViewItem:[tabView_ selectedTabViewItem]];
+ NSIndexSet* indexSet =
+ [NSIndexSet indexSetWithIndex:[self tableIndexFromTabIndex:index]];
+ [self selectRowIndexes:indexSet byExtendingSelection:NO];
+ if ([oldTabViewDelegate_
+ respondsToSelector:@selector(tabView:didSelectTabViewItem:)]) {
+ [oldTabViewDelegate_ tabView:tabView didSelectTabViewItem:tabViewItem];
+ }
+}
+
+- (BOOL) tabView:(NSTabView*)tabView
+ shouldSelectTabViewItem:(NSTabViewItem*)tabViewItem {
+ if ([oldTabViewDelegate_
+ respondsToSelector:@selector(tabView:shouldSelectTabViewItem:)]) {
+ return [oldTabViewDelegate_ tabView:tabView
+ shouldSelectTabViewItem:tabViewItem];
+ }
+ return YES;
+}
+
+- (void) tabView:(NSTabView*)tabView
+ willSelectTabViewItem:(NSTabViewItem*)tabViewItem {
+ if ([oldTabViewDelegate_
+ respondsToSelector:@selector(tabView:willSelectTabViewItem:)]) {
+ [oldTabViewDelegate_ tabView:tabView willSelectTabViewItem:tabViewItem];
+ }
+}
+
+- (NSInteger)tabIndexFromTableIndex:(NSInteger)tableIndex {
+ if (!heading_)
+ return tableIndex;
+ DCHECK(tableIndex > 0);
+ return tableIndex - 1;
+}
+
+- (NSInteger)tableIndexFromTabIndex:(NSInteger)tabIndex {
+ DCHECK_GE(tabIndex, 0);
+ DCHECK_LT(tabIndex, [tabView_ numberOfTabViewItems]);
+ if (!heading_)
+ return tabIndex;
+ return tabIndex + 1;
+}
+
+- (BOOL)isHeadingItem:(id)item {
+ return item && item == heading_.get();
+}
+
+// NSOutlineViewDataSource methods.
+- (NSInteger) outlineView:(NSOutlineView*)outlineView
+ numberOfChildrenOfItem:(id)item {
+ if (!item)
+ return heading_ ? 1 : [tabView_ numberOfTabViewItems];
+ return (item == heading_.get()) ? [tabView_ numberOfTabViewItems] : 0;
+}
+
+- (BOOL)outlineView:(NSOutlineView*)outlineView isItemExpandable:(id)item {
+ return [self isHeadingItem:item];
+}
+
+- (id)outlineView:(NSOutlineView*)outlineView
+ child:(NSInteger)index
+ ofItem:(id)item {
+ if (!item) {
+ return heading_.get() ?
+ heading_.get() : static_cast<id>([tabView_ tabViewItemAtIndex:index]);
+ }
+ return (item == heading_.get()) ? [tabView_ tabViewItemAtIndex:index] : nil;
+}
+
+- (id) outlineView:(NSOutlineView*)outlineView
+ objectValueForTableColumn:(NSTableColumn*)tableColumn
+ byItem:(id)item {
+ if ([item isKindOfClass:[NSTabViewItem class]])
+ return [static_cast<NSTabViewItem*>(item) label];
+ if ([self isHeadingItem:item])
+ return [item uppercaseString];
+ return nil;
+}
+
+// NSOutlineViewDelegate methods.
+- (void)outlineViewSelectionDidChange:(NSNotification*)notification {
+ int row = [self selectedRow];
+ [tabView_ selectTabViewItemAtIndex:[self tabIndexFromTableIndex:row]];
+}
+
+- (BOOL)outlineView:(NSOutlineView *)sender isGroupItem:(id)item {
+ return [self isHeadingItem:item];
+}
+
+- (BOOL)outlineView:(NSOutlineView*)outlineView shouldExpandItem:(id)item {
+ return [self isHeadingItem:item];
+}
+
+- (BOOL)outlineView:(NSOutlineView*)outlineView shouldCollapseItem:(id)item {
+ return NO;
+}
+
+- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item {
+ return ![self isHeadingItem:item];
+}
+
+// -outlineView:shouldShowOutlineCellForItem: is 10.6-only.
+- (NSRect)frameOfOutlineCellAtRow:(NSInteger)row {
+ return NSZeroRect;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/tab_view_picker_table_unittest.mm b/chrome/browser/ui/cocoa/tab_view_picker_table_unittest.mm
new file mode 100644
index 0000000..4b22b3f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_view_picker_table_unittest.mm
@@ -0,0 +1,138 @@
+// 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/ui/cocoa/tab_view_picker_table.h"
+
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+@interface TabViewPickerTableTestPing : NSObject <NSTabViewDelegate> {
+ @public
+ BOOL didSelectItemCalled_;
+}
+@end
+
+@implementation TabViewPickerTableTestPing
+- (void) tabView:(NSTabView*)tabView
+ didSelectTabViewItem:(NSTabViewItem*)tabViewItem {
+ didSelectItemCalled_ = YES;
+}
+@end
+
+namespace {
+
+class TabViewPickerTableTest : public CocoaTest {
+ public:
+ TabViewPickerTableTest() {
+ // Initialize picker table.
+ NSRect frame = NSMakeRect(0, 0, 30, 50);
+ scoped_nsobject<TabViewPickerTable> view(
+ [[TabViewPickerTable alloc] initWithFrame:frame]);
+ view_ = view.get();
+ [view_ setAllowsEmptySelection:NO];
+ [view_ setAllowsMultipleSelection:NO];
+ [view_ addTableColumn:
+ [[[NSTableColumn alloc] initWithIdentifier:nil] autorelease]];
+ [[test_window() contentView] addSubview:view_];
+
+ // Initialize source tab view, with delegate.
+ frame = NSMakeRect(30, 0, 50, 50);
+ scoped_nsobject<NSTabView> tabView(
+ [[NSTabView alloc] initWithFrame:frame]);
+ tabView_ = tabView.get();
+
+ scoped_nsobject<NSTabViewItem> item1(
+ [[NSTabViewItem alloc] initWithIdentifier:nil]);
+ [item1 setLabel:@"label 1"];
+ [tabView_ addTabViewItem:item1];
+
+ scoped_nsobject<NSTabViewItem> item2(
+ [[NSTabViewItem alloc] initWithIdentifier:nil]);
+ [item2 setLabel:@"label 2"];
+ [tabView_ addTabViewItem:item2];
+
+ [tabView_ selectTabViewItemAtIndex:1];
+ [[test_window() contentView] addSubview:tabView_];
+
+ ping_.reset([TabViewPickerTableTestPing new]);
+ [tabView_ setDelegate:ping_.get()];
+
+ // Simulate nib loading.
+ view_->tabView_ = tabView_;
+ [view_ awakeFromNib];
+ }
+
+ TabViewPickerTable* view_;
+ NSTabView* tabView_;
+ scoped_nsobject<TabViewPickerTableTestPing> ping_;
+};
+
+TEST_VIEW(TabViewPickerTableTest, view_)
+
+TEST_F(TabViewPickerTableTest, TestInitialSelectionCorrect) {
+ EXPECT_EQ(1, [view_ selectedRow]);
+}
+
+TEST_F(TabViewPickerTableTest, TestSelectionUpdates) {
+ [tabView_ selectTabViewItemAtIndex:0];
+ EXPECT_EQ(0, [view_ selectedRow]);
+
+ [tabView_ selectTabViewItemAtIndex:1];
+ EXPECT_EQ(1, [view_ selectedRow]);
+}
+
+TEST_F(TabViewPickerTableTest, TestDelegateStillWorks) {
+ EXPECT_FALSE(ping_.get()->didSelectItemCalled_);
+ [tabView_ selectTabViewItemAtIndex:0];
+ EXPECT_TRUE(ping_.get()->didSelectItemCalled_);
+}
+
+TEST_F(TabViewPickerTableTest, RowsCorrect) {
+ EXPECT_EQ(2, [view_ numberOfRows]);
+ EXPECT_EQ(2,
+ [[view_ dataSource] outlineView:view_ numberOfChildrenOfItem:nil]);
+
+ id item;
+ item = [[view_ dataSource] outlineView:view_ child:0 ofItem:nil];
+ EXPECT_NSEQ(@"label 1",
+ [[view_ dataSource] outlineView:view_
+ objectValueForTableColumn:nil // ignored
+ byItem:item]);
+ item = [[view_ dataSource] outlineView:view_ child:1 ofItem:nil];
+ EXPECT_NSEQ(@"label 2",
+ [[view_ dataSource] outlineView:view_
+ objectValueForTableColumn:nil // ignored
+ byItem:item]);
+}
+
+TEST_F(TabViewPickerTableTest, TestListUpdatesTabView) {
+ [view_ selectRowIndexes:[NSIndexSet indexSetWithIndex:0]
+ byExtendingSelection:NO];
+ EXPECT_EQ(0, [view_ selectedRow]); // sanity
+ EXPECT_EQ(0, [tabView_ indexOfTabViewItem:[tabView_ selectedTabViewItem]]);
+}
+
+TEST_F(TabViewPickerTableTest, TestWithHeadingNotEmpty) {
+ [view_ setHeading:@"disregard this"];
+
+ EXPECT_EQ(2, [view_ selectedRow]);
+
+ [tabView_ selectTabViewItemAtIndex:0];
+ EXPECT_EQ(1, [view_ selectedRow]);
+ [tabView_ selectTabViewItemAtIndex:1];
+ EXPECT_EQ(2, [view_ selectedRow]);
+
+ [view_ selectRowIndexes:[NSIndexSet indexSetWithIndex:1]
+ byExtendingSelection:NO];
+ EXPECT_EQ(1, [view_ selectedRow]); // sanity
+ EXPECT_EQ(0, [tabView_ indexOfTabViewItem:[tabView_ selectedTabViewItem]]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/tab_view_unittest.mm b/chrome/browser/ui/cocoa/tab_view_unittest.mm
new file mode 100644
index 0000000..961c3a6
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_view_unittest.mm
@@ -0,0 +1,60 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/tab_view.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class TabViewTest : public CocoaTest {
+ public:
+ TabViewTest() {
+ NSRect frame = NSMakeRect(0, 0, 50, 30);
+ scoped_nsobject<TabView> view([[TabView alloc] initWithFrame:frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ TabView* view_;
+};
+
+TEST_VIEW(TabViewTest, view_)
+
+// Test drawing, mostly to ensure nothing leaks or crashes.
+TEST_F(TabViewTest, Display) {
+ for (int i = 0; i < 5; i++) {
+ for (int j = 0; j < 5; j++) {
+ [view_ setHoverAlpha:i*0.2];
+ [view_ setAlertAlpha:j*0.2];
+ [view_ display];
+ }
+ }
+}
+
+// Test it doesn't crash when asked for its menu with no TabController set.
+TEST_F(TabViewTest, Menu) {
+ EXPECT_FALSE([view_ menu]);
+}
+
+TEST_F(TabViewTest, Glow) {
+ // TODO(viettrungluu): Figure out how to test this, which is timing-sensitive
+ // and which moreover uses |-performSelector:withObject:afterDelay:|.
+
+ // Call |-startAlert|/|-cancelAlert| and make sure it doesn't crash.
+ for (int i = 0; i < 5; i++) {
+ [view_ startAlert];
+ [view_ cancelAlert];
+ }
+ [view_ startAlert];
+ [view_ startAlert];
+ [view_ cancelAlert];
+ [view_ cancelAlert];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/tab_window_controller.h b/chrome/browser/ui/cocoa/tab_window_controller.h
new file mode 100644
index 0000000..7b5f5c8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_window_controller.h
@@ -0,0 +1,177 @@
+// 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_UI_COCOA_TAB_WINDOW_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_TAB_WINDOW_CONTROLLER_H_
+#pragma once
+
+// A class acting as the Objective-C window controller for a window that has
+// tabs which can be dragged around. Tabs can be re-arranged within the same
+// window or dragged into other TabWindowController windows. This class doesn't
+// know anything about the actual tab implementation or model, as that is fairly
+// application-specific. It only provides an API to be overridden by subclasses
+// to fill in the details.
+//
+// This assumes that there will be a view in the nib, connected to
+// |tabContentArea_|, that indicates the content that it switched when switching
+// between tabs. It needs to be a regular NSView, not something like an NSBox
+// because the TabStripController makes certain assumptions about how it can
+// swap out subviews.
+//
+// The tab strip can exist in different orientations and window locations,
+// depending on the return value of -usesVerticalTabs. If NO (the default),
+// the tab strip is placed outside the window's content area, overlapping the
+// title area and window controls and will be stretched to fill the width
+// of the window. If YES, the tab strip is vertical and lives within the
+// window's content area. It will be stretched to fill the window's height.
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+
+@class FastResizeView;
+@class FocusTracker;
+@class TabStripView;
+@class TabView;
+
+@interface TabWindowController : NSWindowController<NSWindowDelegate> {
+ @private
+ IBOutlet FastResizeView* tabContentArea_;
+ // TODO(pinkerton): Figure out a better way to initialize one or the other
+ // w/out needing both to be in the nib.
+ IBOutlet TabStripView* topTabStripView_;
+ IBOutlet TabStripView* sideTabStripView_;
+ NSWindow* overlayWindow_; // Used during dragging for window opacity tricks
+ NSView* cachedContentView_; // Used during dragging for identifying which
+ // view is the proper content area in the overlay
+ // (weak)
+ scoped_nsobject<FocusTracker> focusBeforeOverlay_;
+ scoped_nsobject<NSMutableSet> lockedTabs_;
+ BOOL closeDeferred_; // If YES, call performClose: in removeOverlay:.
+ // Difference between height of window content area and height of the
+ // |tabContentArea_|. Calculated when the window is loaded from the nib and
+ // cached in order to restore the delta when switching tab modes.
+ CGFloat contentAreaHeightDelta_;
+}
+@property(readonly, nonatomic) TabStripView* tabStripView;
+@property(readonly, nonatomic) FastResizeView* tabContentArea;
+
+// Used during tab dragging to turn on/off the overlay window when a tab
+// is torn off. If -deferPerformClose (below) is used, -removeOverlay will
+// cause the controller to be autoreleased before returning.
+- (void)showOverlay;
+- (void)removeOverlay;
+- (NSWindow*)overlayWindow;
+
+// Returns YES if it is ok to constrain the window's frame to fit the screen.
+- (BOOL)shouldConstrainFrameRect;
+
+// A collection of methods, stubbed out in this base class, that provide
+// the implementation of tab dragging based on whatever model is most
+// appropriate.
+
+// Layout the tabs based on the current ordering of the model.
+- (void)layoutTabs;
+
+// Creates a new window by pulling the given tab out and placing it in
+// the new window. Returns the controller for the new window. The size of the
+// new window will be the same size as this window.
+- (TabWindowController*)detachTabToNewWindow:(TabView*)tabView;
+
+// Make room in the tab strip for |tab| at the given x coordinate. Will hide the
+// new tab button while there's a placeholder. Subclasses need to call the
+// superclass implementation.
+- (void)insertPlaceholderForTab:(TabView*)tab
+ frame:(NSRect)frame
+ yStretchiness:(CGFloat)yStretchiness;
+
+// Removes the placeholder installed by |-insertPlaceholderForTab:atLocation:|
+// and restores the new tab button. Subclasses need to call the superclass
+// implementation.
+- (void)removePlaceholder;
+
+// The follow return YES if tab dragging/tab tearing (off the tab strip)/window
+// movement is currently allowed. Any number of things can choose to disable it,
+// such as pending animations. The default implementations always return YES.
+// Subclasses should override as appropriate.
+- (BOOL)tabDraggingAllowed;
+- (BOOL)tabTearingAllowed;
+- (BOOL)windowMovementAllowed;
+
+// Show or hide the new tab button. The button is hidden immediately, but
+// waits until the next call to |-layoutTabs| to show it again.
+- (void)showNewTabButton:(BOOL)show;
+
+// Returns whether or not |tab| can still be fully seen in the tab strip or if
+// its current position would cause it be obscured by things such as the edge
+// of the window or the window decorations. Returns YES only if the entire tab
+// is visible. The default implementation always returns YES.
+- (BOOL)isTabFullyVisible:(TabView*)tab;
+
+// Called to check if the receiver can receive dragged tabs from
+// source. Return YES if so. The default implementation returns NO.
+- (BOOL)canReceiveFrom:(TabWindowController*)source;
+
+// Move a given tab view to the location of the current placeholder. If there is
+// no placeholder, it will go at the end. |controller| is the window controller
+// of a tab being dropped from a different window. It will be nil if the drag is
+// within the window, otherwise the tab is removed from that window before being
+// placed into this one. The implementation will call |-removePlaceholder| since
+// the drag is now complete. This also calls |-layoutTabs| internally so
+// clients do not need to call it again.
+- (void)moveTabView:(NSView*)view
+ fromController:(TabWindowController*)controller;
+
+// Number of tabs in the tab strip. Useful, for example, to know if we're
+// dragging the only tab in the window. This includes pinned tabs (both live
+// and not).
+- (NSInteger)numberOfTabs;
+
+// YES if there are tabs in the tab strip which have content, allowing for
+// the notion of tabs in the tab strip that are placeholders but currently have
+// no content.
+- (BOOL)hasLiveTabs;
+
+// Return the view of the selected tab.
+- (NSView *)selectedTabView;
+
+// The title of the selected tab.
+- (NSString*)selectedTabTitle;
+
+// Called to check whether or not this controller's window has a tab strip (YES
+// if it does, NO otherwise). The default implementation returns YES.
+- (BOOL)hasTabStrip;
+
+// Returns YES if the tab strip lives in the window content area alongside the
+// tab contents. Returns NO if the tab strip is outside the window content
+// area, along the top of the window.
+- (BOOL)useVerticalTabs;
+
+// Get/set whether a particular tab is draggable between windows.
+- (BOOL)isTabDraggable:(NSView*)tabView;
+- (void)setTab:(NSView*)tabView isDraggable:(BOOL)draggable;
+
+// Tell the window that it needs to call performClose: as soon as the current
+// drag is complete. This prevents a window (and its overlay) from going away
+// during a drag.
+- (void)deferPerformClose;
+
+@end
+
+@interface TabWindowController(ProtectedMethods)
+// Tells the tab strip to forget about this tab in preparation for it being
+// put into a different tab strip, such as during a drop on another window.
+- (void)detachTabView:(NSView*)view;
+
+// Toggles from one display mode of the tab strip to another. Will automatically
+// call -layoutSubviews to reposition other content.
+- (void)toggleTabStripDisplayMode;
+
+// Called when the size of the window content area has changed. Override to
+// position specific views. Base class implementation does nothing.
+- (void)layoutSubviews;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_TAB_WINDOW_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/tab_window_controller.mm b/chrome/browser/ui/cocoa/tab_window_controller.mm
new file mode 100644
index 0000000..70504be
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tab_window_controller.mm
@@ -0,0 +1,351 @@
+// 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/ui/cocoa/tab_window_controller.h"
+
+#include "app/theme_provider.h"
+#include "base/logging.h"
+#import "chrome/browser/ui/cocoa/focus_tracker.h"
+#import "chrome/browser/ui/cocoa/tab_strip_view.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+
+@interface TabWindowController(PRIVATE)
+- (void)setUseOverlay:(BOOL)useOverlay;
+@end
+
+@interface TabWindowOverlayWindow : NSWindow
+@end
+
+@implementation TabWindowOverlayWindow
+
+- (ThemeProvider*)themeProvider {
+ if ([self parentWindow])
+ return [[[self parentWindow] windowController] themeProvider];
+ return NULL;
+}
+
+- (ThemedWindowStyle)themedWindowStyle {
+ if ([self parentWindow])
+ return [[[self parentWindow] windowController] themedWindowStyle];
+ return NO;
+}
+
+- (NSPoint)themePatternPhase {
+ if ([self parentWindow])
+ return [[[self parentWindow] windowController] themePatternPhase];
+ return NSZeroPoint;
+}
+
+@end
+
+@implementation TabWindowController
+@synthesize tabContentArea = tabContentArea_;
+
+- (id)initWithWindow:(NSWindow*)window {
+ if ((self = [super initWithWindow:window]) != nil) {
+ lockedTabs_.reset([[NSMutableSet alloc] initWithCapacity:10]);
+ }
+ return self;
+}
+
+// Add the side tab strip to the left side of the window's content area,
+// making it fill the full height of the content area.
+- (void)addSideTabStripToWindow {
+ NSView* contentView = [[self window] contentView];
+ NSRect contentFrame = [contentView frame];
+ NSRect sideStripFrame =
+ NSMakeRect(0, 0,
+ NSWidth([sideTabStripView_ frame]),
+ NSHeight(contentFrame));
+ [sideTabStripView_ setFrame:sideStripFrame];
+ [contentView addSubview:sideTabStripView_];
+}
+
+// Add the top tab strop to the window, above the content box and add it to the
+// view hierarchy as a sibling of the content view so it can overlap with the
+// window frame.
+- (void)addTopTabStripToWindow {
+ NSRect contentFrame = [tabContentArea_ frame];
+ NSRect tabFrame =
+ NSMakeRect(0, NSMaxY(contentFrame),
+ NSWidth(contentFrame),
+ NSHeight([topTabStripView_ frame]));
+ [topTabStripView_ setFrame:tabFrame];
+ NSView* contentParent = [[[self window] contentView] superview];
+ [contentParent addSubview:topTabStripView_];
+}
+
+- (void)windowDidLoad {
+ // Cache the difference in height between the window content area and the
+ // tab content area.
+ NSRect tabFrame = [tabContentArea_ frame];
+ NSRect contentFrame = [[[self window] contentView] frame];
+ contentAreaHeightDelta_ = NSHeight(contentFrame) - NSHeight(tabFrame);
+
+ if ([self hasTabStrip]) {
+ if ([self useVerticalTabs]) {
+ // No top tabstrip so remove the tabContentArea offset.
+ tabFrame.size.height = contentFrame.size.height;
+ [tabContentArea_ setFrame:tabFrame];
+ [self addSideTabStripToWindow];
+ } else {
+ [self addTopTabStripToWindow];
+ }
+ } else {
+ // No top tabstrip so remove the tabContentArea offset.
+ tabFrame.size.height = contentFrame.size.height;
+ [tabContentArea_ setFrame:tabFrame];
+ }
+}
+
+// Toggles from one display mode of the tab strip to another. Will automatically
+// call -layoutSubviews to reposition other content.
+- (void)toggleTabStripDisplayMode {
+ // Adjust the size of the tab contents to either use more or less space,
+ // depending on the direction of the toggle. This needs to be done prior to
+ // adding back in the top tab strip as its position is based off the MaxY
+ // of the tab content area.
+ BOOL useVertical = [self useVerticalTabs];
+ NSRect tabContentsFrame = [tabContentArea_ frame];
+ tabContentsFrame.size.height += useVertical ?
+ contentAreaHeightDelta_ : -contentAreaHeightDelta_;
+ [tabContentArea_ setFrame:tabContentsFrame];
+
+ if (useVertical) {
+ // Remove the top tab strip and add the sidebar in.
+ [topTabStripView_ removeFromSuperview];
+ [self addSideTabStripToWindow];
+ } else {
+ // Remove the side tab strip and add the top tab strip as a sibling of the
+ // window's content area.
+ [sideTabStripView_ removeFromSuperview];
+ NSRect tabContentsFrame = [tabContentArea_ frame];
+ tabContentsFrame.size.height -= contentAreaHeightDelta_;
+ [tabContentArea_ setFrame:tabContentsFrame];
+ [self addTopTabStripToWindow];
+ }
+
+ [self layoutSubviews];
+}
+
+// Return the appropriate tab strip based on whether or not side tabs are
+// enabled.
+- (TabStripView*)tabStripView {
+ if ([self useVerticalTabs])
+ return sideTabStripView_;
+ return topTabStripView_;
+}
+
+- (void)removeOverlay {
+ [self setUseOverlay:NO];
+ if (closeDeferred_) {
+ // See comment in BrowserWindowCocoa::Close() about orderOut:.
+ [[self window] orderOut:self];
+ [[self window] performClose:self]; // Autoreleases the controller.
+ }
+}
+
+- (void)showOverlay {
+ [self setUseOverlay:YES];
+}
+
+// if |useOverlay| is true, we're moving views into the overlay's content
+// area. If false, we're moving out of the overlay back into the window's
+// content.
+- (void)moveViewsBetweenWindowAndOverlay:(BOOL)useOverlay {
+ if (useOverlay) {
+ [[[overlayWindow_ contentView] superview] addSubview:[self tabStripView]];
+ // Add the original window's content view as a subview of the overlay
+ // window's content view. We cannot simply use setContentView: here because
+ // the overlay window has a different content size (due to it being
+ // borderless).
+ [[overlayWindow_ contentView] addSubview:cachedContentView_];
+ } else {
+ [[self window] setContentView:cachedContentView_];
+ // The TabStripView always needs to be in front of the window's content
+ // view and therefore it should always be added after the content view is
+ // set.
+ [[[[self window] contentView] superview] addSubview:[self tabStripView]];
+ [[[[self window] contentView] superview] updateTrackingAreas];
+ }
+}
+
+// If |useOverlay| is YES, creates a new overlay window and puts the tab strip
+// and the content area inside of it. This allows it to have a different opacity
+// from the title bar. If NO, returns everything to the previous state and
+// destroys the overlay window until it's needed again. The tab strip and window
+// contents are returned to the original window.
+- (void)setUseOverlay:(BOOL)useOverlay {
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(removeOverlay)
+ object:nil];
+ NSWindow* window = [self window];
+ if (useOverlay && !overlayWindow_) {
+ DCHECK(!cachedContentView_);
+ overlayWindow_ = [[TabWindowOverlayWindow alloc]
+ initWithContentRect:[window frame]
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:YES];
+ [overlayWindow_ setTitle:@"overlay"];
+ [overlayWindow_ setBackgroundColor:[NSColor clearColor]];
+ [overlayWindow_ setOpaque:NO];
+ [overlayWindow_ setDelegate:self];
+ cachedContentView_ = [window contentView];
+ [window addChildWindow:overlayWindow_ ordered:NSWindowAbove];
+ // Sets explictly nil to the responder and then restores it.
+ // Leaving the first responder non-null here
+ // causes [RenderWidgethostViewCocoa resignFirstResponder] and
+ // following RenderWidgetHost::Blur(), which results unexpected
+ // focus lost.
+ focusBeforeOverlay_.reset([[FocusTracker alloc] initWithWindow:window]);
+ [window makeFirstResponder:nil];
+ [self moveViewsBetweenWindowAndOverlay:useOverlay];
+ [overlayWindow_ orderFront:nil];
+ } else if (!useOverlay && overlayWindow_) {
+ DCHECK(cachedContentView_);
+ [window setContentView:cachedContentView_];
+ [self moveViewsBetweenWindowAndOverlay:useOverlay];
+ [focusBeforeOverlay_ restoreFocusInWindow:window];
+ focusBeforeOverlay_.reset(nil);
+ [window display];
+ [window removeChildWindow:overlayWindow_];
+ [overlayWindow_ orderOut:nil];
+ [overlayWindow_ release];
+ overlayWindow_ = nil;
+ cachedContentView_ = nil;
+ } else {
+ NOTREACHED();
+ }
+}
+
+- (NSWindow*)overlayWindow {
+ return overlayWindow_;
+}
+
+- (BOOL)shouldConstrainFrameRect {
+ // If we currently have an overlay window, do not attempt to change the
+ // window's size, as our overlay window doesn't know how to resize properly.
+ return overlayWindow_ == nil;
+}
+
+- (BOOL)canReceiveFrom:(TabWindowController*)source {
+ // subclass must implement
+ NOTIMPLEMENTED();
+ return NO;
+}
+
+- (void)moveTabView:(NSView*)view
+ fromController:(TabWindowController*)dragController {
+ NOTIMPLEMENTED();
+}
+
+- (NSView*)selectedTabView {
+ NOTIMPLEMENTED();
+ return nil;
+}
+
+- (void)layoutTabs {
+ // subclass must implement
+ NOTIMPLEMENTED();
+}
+
+- (TabWindowController*)detachTabToNewWindow:(TabView*)tabView {
+ // subclass must implement
+ NOTIMPLEMENTED();
+ return NULL;
+}
+
+- (void)insertPlaceholderForTab:(TabView*)tab
+ frame:(NSRect)frame
+ yStretchiness:(CGFloat)yStretchiness {
+ [self showNewTabButton:NO];
+}
+
+- (void)removePlaceholder {
+ [self showNewTabButton:YES];
+}
+
+- (BOOL)tabDraggingAllowed {
+ return YES;
+}
+
+- (BOOL)tabTearingAllowed {
+ return YES;
+}
+
+- (BOOL)windowMovementAllowed {
+ return YES;
+}
+
+- (BOOL)isTabFullyVisible:(TabView*)tab {
+ // Subclasses should implement this, but it's not necessary.
+ return YES;
+}
+
+- (void)showNewTabButton:(BOOL)show {
+ // subclass must implement
+ NOTIMPLEMENTED();
+}
+
+- (void)detachTabView:(NSView*)view {
+ // subclass must implement
+ NOTIMPLEMENTED();
+}
+
+- (NSInteger)numberOfTabs {
+ // subclass must implement
+ NOTIMPLEMENTED();
+ return 0;
+}
+
+- (BOOL)hasLiveTabs {
+ // subclass must implement
+ NOTIMPLEMENTED();
+ return NO;
+}
+
+- (NSString*)selectedTabTitle {
+ // subclass must implement
+ NOTIMPLEMENTED();
+ return @"";
+}
+
+- (BOOL)hasTabStrip {
+ // Subclasses should implement this.
+ NOTIMPLEMENTED();
+ return YES;
+}
+
+- (BOOL)useVerticalTabs {
+ // Subclasses should implement this.
+ NOTIMPLEMENTED();
+ return NO;
+}
+
+- (BOOL)isTabDraggable:(NSView*)tabView {
+ return ![lockedTabs_ containsObject:tabView];
+}
+
+- (void)setTab:(NSView*)tabView isDraggable:(BOOL)draggable {
+ if (draggable)
+ [lockedTabs_ removeObject:tabView];
+ else
+ [lockedTabs_ addObject:tabView];
+}
+
+// Tell the window that it needs to call performClose: as soon as the current
+// drag is complete. This prevents a window (and its overlay) from going away
+// during a drag.
+- (void)deferPerformClose {
+ closeDeferred_ = YES;
+}
+
+// Called when the size of the window content area has changed. Override to
+// position specific views. Base class implementation does nothing.
+- (void)layoutSubviews {
+ NOTIMPLEMENTED();
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/table_model_array_controller.h b/chrome/browser/ui/cocoa/table_model_array_controller.h
new file mode 100644
index 0000000..11af327
--- /dev/null
+++ b/chrome/browser/ui/cocoa/table_model_array_controller.h
@@ -0,0 +1,54 @@
+// 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_UI_COCOA_TABLE_MODEL_ARRAY_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_TABLE_MODEL_ARRAY_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "app/table_model_observer.h"
+#include "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+
+class RemoveRowsObserverBridge;
+class RemoveRowsTableModel;
+@class TableModelArrayController;
+
+// This class functions as an adapter from a RemoveRowsTableModel to a Cocoa
+// NSArrayController, to be used with bindings.
+// It maps the CanRemoveRows method to its canRemove property, and exposes
+// RemoveRows and RemoveAll as actions (remove: and removeAll:).
+// If the table model has groups, these are inserted into the list of arranged
+// objects as group rows.
+// The designated initializer is the same as for NSArrayController,
+// initWithContent:, but usually this class is instantiated from a nib file.
+// Clicking on a group row selects all rows belonging to that group, like it
+// does in a Windows table_view.
+// In order to show group rows, this class must be the delegate of the
+// NSTableView.
+@interface TableModelArrayController : NSArrayController<NSTableViewDelegate> {
+ @private
+ RemoveRowsTableModel* model_; // weak
+ scoped_ptr<RemoveRowsObserverBridge> tableObserver_;
+ scoped_nsobject<NSDictionary> columns_;
+ scoped_nsobject<NSString> groupTitle_;
+}
+
+// Bind this controller to the given model.
+// |columns| is a dictionary mapping table column bindings to NSNumbers
+// containing the column identifier in the TableModel.
+// |groupTitleColumn| is the column in the table that should display the group
+// title for a group row, usually the first column. If the model doesn't have
+// groups, it can be nil.
+- (void)bindToTableModel:(RemoveRowsTableModel*)model
+ withColumns:(NSDictionary*)columns
+ groupTitleColumn:(NSString*)groupTitleColumn;
+
+- (IBAction)removeAll:(id)sender;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_TABLE_MODEL_ARRAY_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/table_model_array_controller.mm b/chrome/browser/ui/cocoa/table_model_array_controller.mm
new file mode 100644
index 0000000..e732127
--- /dev/null
+++ b/chrome/browser/ui/cocoa/table_model_array_controller.mm
@@ -0,0 +1,246 @@
+// 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/ui/cocoa/table_model_array_controller.h"
+
+#include "app/table_model.h"
+#include "base/logging.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/remove_rows_table_model.h"
+
+@interface TableModelArrayController (PrivateMethods)
+
+- (NSUInteger)offsetForGroupID:(int)groupID;
+- (NSUInteger)offsetForGroupID:(int)groupID startingOffset:(NSUInteger)offset;
+- (NSIndexSet*)controllerRowsForModelRowsInRange:(NSRange)range;
+- (void)setModelRows:(RemoveRowsTableModel::Rows*)modelRows
+ fromControllerRows:(NSIndexSet*)rows;
+- (void)modelDidChange;
+- (void)modelDidAddItemsInRange:(NSRange)range;
+- (void)modelDidRemoveItemsInRange:(NSRange)range;
+- (NSDictionary*)columnValuesForRow:(NSInteger)row;
+
+@end
+
+// Observer for a RemoveRowsTableModel.
+class RemoveRowsObserverBridge : public TableModelObserver {
+ public:
+ RemoveRowsObserverBridge(TableModelArrayController* controller)
+ : controller_(controller) {}
+ virtual ~RemoveRowsObserverBridge() {}
+
+ // TableModelObserver methods
+ virtual void OnModelChanged();
+ virtual void OnItemsChanged(int start, int length);
+ virtual void OnItemsAdded(int start, int length);
+ virtual void OnItemsRemoved(int start, int length);
+
+ private:
+ TableModelArrayController* controller_; // weak
+};
+
+void RemoveRowsObserverBridge::OnModelChanged() {
+ [controller_ modelDidChange];
+}
+
+void RemoveRowsObserverBridge::OnItemsChanged(int start, int length) {
+ OnItemsRemoved(start, length);
+ OnItemsAdded(start, length);
+}
+
+void RemoveRowsObserverBridge::OnItemsAdded(int start, int length) {
+ [controller_ modelDidAddItemsInRange:NSMakeRange(start, length)];
+}
+
+void RemoveRowsObserverBridge::OnItemsRemoved(int start, int length) {
+ [controller_ modelDidRemoveItemsInRange:NSMakeRange(start, length)];
+}
+
+@implementation TableModelArrayController
+
+static NSString* const kIsGroupRow = @"_is_group_row";
+static NSString* const kGroupID = @"_group_id";
+
+- (void)bindToTableModel:(RemoveRowsTableModel*)model
+ withColumns:(NSDictionary*)columns
+ groupTitleColumn:(NSString*)groupTitleColumn {
+ model_ = model;
+ tableObserver_.reset(new RemoveRowsObserverBridge(self));
+ columns_.reset([columns copy]);
+ groupTitle_.reset([groupTitleColumn copy]);
+ model_->SetObserver(tableObserver_.get());
+ [self modelDidChange];
+}
+
+- (void)modelDidChange {
+ NSIndexSet* indexes = [NSIndexSet indexSetWithIndexesInRange:
+ NSMakeRange(0, [[self arrangedObjects] count])];
+ [self removeObjectsAtArrangedObjectIndexes:indexes];
+ if (model_->HasGroups()) {
+ const TableModel::Groups& groups = model_->GetGroups();
+ DCHECK(groupTitle_.get());
+ for (TableModel::Groups::const_iterator it = groups.begin();
+ it != groups.end(); ++it) {
+ NSDictionary* group = [NSDictionary dictionaryWithObjectsAndKeys:
+ base::SysWideToNSString(it->title), groupTitle_.get(),
+ [NSNumber numberWithBool:YES], kIsGroupRow,
+ nil];
+ [self addObject:group];
+ }
+ }
+ [self modelDidAddItemsInRange:NSMakeRange(0, model_->RowCount())];
+}
+
+- (NSUInteger)offsetForGroupID:(int)groupID startingOffset:(NSUInteger)offset {
+ const TableModel::Groups& groups = model_->GetGroups();
+ DCHECK_GT(offset, 0u);
+ for (NSUInteger i = offset - 1; i < groups.size(); ++i) {
+ if (groups[i].id == groupID)
+ return i + 1;
+ }
+ NOTREACHED();
+ return NSNotFound;
+}
+
+- (NSUInteger)offsetForGroupID:(int)groupID {
+ return [self offsetForGroupID:groupID startingOffset:1];
+}
+
+- (int)groupIDForControllerRow:(NSUInteger)row {
+ NSDictionary* values = [[self arrangedObjects] objectAtIndex:row];
+ return [[values objectForKey:kGroupID] intValue];
+}
+
+- (void)setModelRows:(RemoveRowsTableModel::Rows*)modelRows
+ fromControllerRows:(NSIndexSet*)rows {
+ if ([rows count] == 0)
+ return;
+
+ if (!model_->HasGroups()) {
+ for (NSUInteger i = [rows firstIndex];
+ i != NSNotFound;
+ i = [rows indexGreaterThanIndex:i]) {
+ modelRows->insert(i);
+ }
+ return;
+ }
+
+ NSUInteger offset = 1;
+ for (NSUInteger i = [rows firstIndex];
+ i != NSNotFound;
+ i = [rows indexGreaterThanIndex:i]) {
+ int group = [self groupIDForControllerRow:i];
+ offset = [self offsetForGroupID:group startingOffset:offset];
+ modelRows->insert(i - offset);
+ }
+}
+
+- (NSIndexSet*)controllerRowsForModelRowsInRange:(NSRange)range {
+ if (!model_->HasGroups())
+ return [NSIndexSet indexSetWithIndexesInRange:range];
+ NSMutableIndexSet* indexes = [NSMutableIndexSet indexSet];
+ NSUInteger offset = 1;
+ for (NSUInteger i = range.location; i < NSMaxRange(range); ++i) {
+ int group = model_->GetGroupID(i);
+ offset = [self offsetForGroupID:group startingOffset:offset];
+ [indexes addIndex:i + offset];
+ }
+ return indexes;
+}
+
+- (void)modelDidAddItemsInRange:(NSRange)range {
+ NSMutableArray* rows = [NSMutableArray arrayWithCapacity:range.length];
+ for (NSUInteger i=range.location; i<NSMaxRange(range); ++i)
+ [rows addObject:[self columnValuesForRow:i]];
+ [self insertObjects:rows
+ atArrangedObjectIndexes:[self controllerRowsForModelRowsInRange:range]];
+}
+
+- (void)modelDidRemoveItemsInRange:(NSRange)range {
+ NSMutableIndexSet* indexes =
+ [NSMutableIndexSet indexSetWithIndexesInRange:range];
+ if (model_->HasGroups()) {
+ // When this method is called, the model has already removed items, so
+ // accessing items in the model from |range.location| on may not be possible
+ // anymore. Therefore we use the item right before that, if it exists.
+ NSUInteger offset = 0;
+ if (range.location > 0) {
+ int last_group = model_->GetGroupID(range.location - 1);
+ offset = [self offsetForGroupID:last_group];
+ }
+ [indexes shiftIndexesStartingAtIndex:0 by:offset];
+ for (NSUInteger row = range.location + offset;
+ row < NSMaxRange(range) + offset;
+ ++row) {
+ if ([self tableView:nil isGroupRow:row]) {
+ // Skip over group rows.
+ [indexes shiftIndexesStartingAtIndex:row by:1];
+ offset++;
+ }
+ }
+ }
+ [self removeObjectsAtArrangedObjectIndexes:indexes];
+}
+
+- (NSDictionary*)columnValuesForRow:(NSInteger)row {
+ NSMutableDictionary* dict = [NSMutableDictionary dictionary];
+ if (model_->HasGroups()) {
+ [dict setObject:[NSNumber numberWithInt:model_->GetGroupID(row)]
+ forKey:kGroupID];
+ }
+ for (NSString* identifier in columns_.get()) {
+ int column_id = [[columns_ objectForKey:identifier] intValue];
+ std::wstring text = model_->GetText(row, column_id);
+ [dict setObject:base::SysWideToNSString(text) forKey:identifier];
+ }
+ return dict;
+}
+
+// Overridden from NSArrayController -----------------------------------------
+
+- (BOOL)canRemove {
+ if (!model_)
+ return NO;
+ RemoveRowsTableModel::Rows rows;
+ [self setModelRows:&rows fromControllerRows:[self selectionIndexes]];
+ return model_->CanRemoveRows(rows);
+}
+
+- (IBAction)remove:(id)sender {
+ RemoveRowsTableModel::Rows rows;
+ [self setModelRows:&rows fromControllerRows:[self selectionIndexes]];
+ model_->RemoveRows(rows);
+}
+
+// Table View Delegate --------------------------------------------------------
+
+- (BOOL)tableView:(NSTableView*)tv isGroupRow:(NSInteger)row {
+ NSDictionary* values = [[self arrangedObjects] objectAtIndex:row];
+ return [[values objectForKey:kIsGroupRow] boolValue];
+}
+
+- (NSIndexSet*)tableView:(NSTableView*)tableView
+ selectionIndexesForProposedSelection:(NSIndexSet*)proposedIndexes {
+ NSMutableIndexSet* indexes = [proposedIndexes mutableCopy];
+ for (NSUInteger i = [proposedIndexes firstIndex];
+ i != NSNotFound;
+ i = [proposedIndexes indexGreaterThanIndex:i]) {
+ if ([self tableView:tableView isGroupRow:i]) {
+ [indexes removeIndex:i];
+ NSUInteger row = i + 1;
+ while (row < [[self arrangedObjects] count] &&
+ ![self tableView:tableView isGroupRow:row])
+ [indexes addIndex:row++];
+ }
+ }
+ return indexes;
+}
+
+// Actions --------------------------------------------------------------------
+
+- (IBAction)removeAll:(id)sender {
+ model_->RemoveAll();
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/table_model_array_controller_unittest.mm b/chrome/browser/ui/cocoa/table_model_array_controller_unittest.mm
new file mode 100644
index 0000000..c60c4cf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/table_model_array_controller_unittest.mm
@@ -0,0 +1,172 @@
+// 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/ui/cocoa/table_model_array_controller.h"
+
+#include "base/auto_reset.h"
+#include "base/command_line.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/mock_plugin_exceptions_table_model.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/test/testing_profile.h"
+#include "grit/generated_resources.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/gtest_mac.h"
+#include "webkit/glue/plugins/plugin_list.h"
+#include "webkit/glue/plugins/webplugininfo.h"
+
+namespace {
+
+class TableModelArrayControllerTest : public CocoaTest {
+ public:
+ TableModelArrayControllerTest()
+ : command_line_(CommandLine::ForCurrentProcess(),
+ *CommandLine::ForCurrentProcess()) {}
+
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+
+ CommandLine::ForCurrentProcess()->AppendSwitch(
+ switches::kEnableResourceContentSettings);
+
+ TestingProfile* profile = browser_helper_.profile();
+ HostContentSettingsMap* map = profile->GetHostContentSettingsMap();
+
+ HostContentSettingsMap::Pattern example_com("[*.]example.com");
+ HostContentSettingsMap::Pattern moose_org("[*.]moose.org");
+ map->SetContentSetting(example_com,
+ CONTENT_SETTINGS_TYPE_PLUGINS,
+ "a-foo",
+ CONTENT_SETTING_ALLOW);
+ map->SetContentSetting(moose_org,
+ CONTENT_SETTINGS_TYPE_PLUGINS,
+ "b-bar",
+ CONTENT_SETTING_BLOCK);
+ map->SetContentSetting(example_com,
+ CONTENT_SETTINGS_TYPE_PLUGINS,
+ "b-bar",
+ CONTENT_SETTING_ALLOW);
+
+ model_.reset(new MockPluginExceptionsTableModel(map, NULL));
+
+ NPAPI::PluginList::PluginMap plugins;
+ WebPluginInfo foo_plugin;
+ foo_plugin.path = FilePath(FILE_PATH_LITERAL("a-foo"));
+ foo_plugin.name = ASCIIToUTF16("FooPlugin");
+ foo_plugin.enabled = true;
+ PluginGroup* foo_group = PluginGroup::FromWebPluginInfo(foo_plugin);
+ plugins[foo_group->identifier()] = linked_ptr<PluginGroup>(foo_group);
+ WebPluginInfo bar_plugin;
+ bar_plugin.path = FilePath(FILE_PATH_LITERAL("b-bar"));
+ bar_plugin.name = ASCIIToUTF16("BarPlugin");
+ bar_plugin.enabled = true;
+ PluginGroup* bar_group = PluginGroup::FromWebPluginInfo(bar_plugin);
+ plugins[bar_group->identifier()] = linked_ptr<PluginGroup>(bar_group);
+ WebPluginInfo blurp_plugin;
+ blurp_plugin.path = FilePath(FILE_PATH_LITERAL("c-blurp"));
+ blurp_plugin.name = ASCIIToUTF16("BlurpPlugin");
+ blurp_plugin.enabled = true;
+ PluginGroup* blurp_group = PluginGroup::FromWebPluginInfo(blurp_plugin);
+ plugins[blurp_group->identifier()] = linked_ptr<PluginGroup>(blurp_group);
+
+ model_->set_plugins(plugins);
+ model_->LoadSettings();
+
+ id content = [NSMutableArray array];
+ controller_.reset(
+ [[TableModelArrayController alloc] initWithContent:content]);
+ NSDictionary* columns = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithInt:IDS_EXCEPTIONS_HOSTNAME_HEADER], @"title",
+ [NSNumber numberWithInt:IDS_EXCEPTIONS_ACTION_HEADER], @"action",
+ nil];
+ [controller_.get() bindToTableModel:model_.get()
+ withColumns:columns
+ groupTitleColumn:@"title"];
+ }
+
+ protected:
+ BrowserTestHelper browser_helper_;
+ scoped_ptr<MockPluginExceptionsTableModel> model_;
+ scoped_nsobject<TableModelArrayController> controller_;
+
+ private:
+ AutoReset<CommandLine> command_line_;
+};
+
+TEST_F(TableModelArrayControllerTest, CheckTitles) {
+ NSArray* titles = [[controller_.get() arrangedObjects] valueForKey:@"title"];
+ EXPECT_NSEQ(@"(\n"
+ @" FooPlugin,\n"
+ @" \"[*.]example.com\",\n"
+ @" BarPlugin,\n"
+ @" \"[*.]example.com\",\n"
+ @" \"[*.]moose.org\"\n"
+ @")",
+ [titles description]);
+}
+
+TEST_F(TableModelArrayControllerTest, RemoveRows) {
+ NSArrayController* controller = controller_.get();
+ [controller setSelectionIndex:1];
+ [controller remove:nil];
+ NSArray* titles = [[controller arrangedObjects] valueForKey:@"title"];
+ EXPECT_NSEQ(@"(\n"
+ @" BarPlugin,\n"
+ @" \"[*.]example.com\",\n"
+ @" \"[*.]moose.org\"\n"
+ @")",
+ [titles description]);
+
+ [controller setSelectionIndex:2];
+ [controller remove:nil];
+ titles = [[controller arrangedObjects] valueForKey:@"title"];
+ EXPECT_NSEQ(@"(\n"
+ @" BarPlugin,\n"
+ @" \"[*.]example.com\"\n"
+ @")",
+ [titles description]);
+}
+
+TEST_F(TableModelArrayControllerTest, RemoveAll) {
+ [controller_.get() removeAll:nil];
+ EXPECT_EQ(0u, [[controller_.get() arrangedObjects] count]);
+}
+
+TEST_F(TableModelArrayControllerTest, AddException) {
+ TestingProfile* profile = browser_helper_.profile();
+ HostContentSettingsMap* map = profile->GetHostContentSettingsMap();
+ HostContentSettingsMap::Pattern example_com("[*.]example.com");
+ map->SetContentSetting(example_com,
+ CONTENT_SETTINGS_TYPE_PLUGINS,
+ "c-blurp",
+ CONTENT_SETTING_BLOCK);
+
+ NSArrayController* controller = controller_.get();
+ NSArray* titles = [[controller arrangedObjects] valueForKey:@"title"];
+ EXPECT_NSEQ(@"(\n"
+ @" FooPlugin,\n"
+ @" \"[*.]example.com\",\n"
+ @" BarPlugin,\n"
+ @" \"[*.]example.com\",\n"
+ @" \"[*.]moose.org\",\n"
+ @" BlurpPlugin,\n"
+ @" \"[*.]example.com\"\n"
+ @")",
+ [titles description]);
+ NSMutableIndexSet* indexes = [NSMutableIndexSet indexSetWithIndex:1];
+ [indexes addIndex:6];
+ [controller setSelectionIndexes:indexes];
+ [controller remove:nil];
+ titles = [[controller arrangedObjects] valueForKey:@"title"];
+ EXPECT_NSEQ(@"(\n"
+ @" BarPlugin,\n"
+ @" \"[*.]example.com\",\n"
+ @" \"[*.]moose.org\"\n"
+ @")",
+ [titles description]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/table_row_nsimage_cache.h b/chrome/browser/ui/cocoa/table_row_nsimage_cache.h
new file mode 100644
index 0000000..c42107f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/table_row_nsimage_cache.h
@@ -0,0 +1,55 @@
+// 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_UI_COCOA_TABLE_ROW_NSIMAGE_CACHE_H_
+#define CHROME_BROWSER_UI_COCOA_TABLE_ROW_NSIMAGE_CACHE_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+
+class SkBitmap;
+
+// There are several dialogs that display tabular data with one SkBitmap
+// per row. This class converts these SkBitmaps to NSImages on demand, and
+// caches the results.
+class TableRowNSImageCache {
+ public:
+ // Interface this cache expects for its table model.
+ class Table {
+ public:
+ // Returns the number of rows in the table.
+ virtual int RowCount() const = 0;
+
+ // Returns the icon of the |row|th row.
+ virtual SkBitmap GetIcon(int row) const = 0;
+
+ protected:
+ virtual ~Table() {}
+ };
+
+ // |model| must outlive the cache.
+ explicit TableRowNSImageCache(Table* model);
+
+ // Lazily converts the image at the given row and caches it in |icon_images_|.
+ NSImage* GetImageForRow(int row);
+
+ // Call these functions every time the table changes, to update the cache.
+ void OnModelChanged();
+ void OnItemsChanged(int start, int length);
+ void OnItemsAdded(int start, int length);
+ void OnItemsRemoved(int start, int length);
+
+ private:
+ // The table model we query for row count and icons.
+ Table* model_; // weak
+
+ // Stores strong NSImage refs for icons. If an entry is NULL, it will be
+ // created in GetImageForRow().
+ scoped_nsobject<NSPointerArray> icon_images_;
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_TABLE_ROW_NSIMAGE_CACHE_H_
+
diff --git a/chrome/browser/ui/cocoa/table_row_nsimage_cache.mm b/chrome/browser/ui/cocoa/table_row_nsimage_cache.mm
new file mode 100644
index 0000000..ae60dd6
--- /dev/null
+++ b/chrome/browser/ui/cocoa/table_row_nsimage_cache.mm
@@ -0,0 +1,79 @@
+// 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 "chrome/browser/ui/cocoa/table_row_nsimage_cache.h"
+
+#include "base/logging.h"
+#include "skia/ext/skia_utils_mac.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+TableRowNSImageCache::TableRowNSImageCache(Table* model)
+ : model_(model),
+ icon_images_([[NSPointerArray alloc] initWithOptions:
+ NSPointerFunctionsStrongMemory |
+ NSPointerFunctionsObjectPersonality]) {
+ int count = model_->RowCount();
+ [icon_images_ setCount:count];
+}
+
+void TableRowNSImageCache::OnModelChanged() {
+ int count = model_->RowCount();
+ [icon_images_ setCount:0];
+ [icon_images_ setCount:count];
+}
+
+void TableRowNSImageCache::OnItemsChanged(int start, int length) {
+ DCHECK_LE(start + length, static_cast<int>([icon_images_ count]));
+ for (int i = start; i < (start + length); ++i) {
+ [icon_images_ replacePointerAtIndex:i withPointer:NULL];
+ }
+ DCHECK_EQ(model_->RowCount(),
+ static_cast<int>([icon_images_ count]));
+}
+
+void TableRowNSImageCache::OnItemsAdded(int start, int length) {
+ DCHECK_LE(start, static_cast<int>([icon_images_ count]));
+
+ // -[NSPointerArray insertPointer:atIndex:] throws if index == count.
+ // Instead expand the array with NULLs.
+ if (start == static_cast<int>([icon_images_ count])) {
+ [icon_images_ setCount:start + length];
+ } else {
+ for (int i = 0; i < length; ++i) {
+ [icon_images_ insertPointer:NULL atIndex:start]; // Values slide up.
+ }
+ }
+ DCHECK_EQ(model_->RowCount(),
+ static_cast<int>([icon_images_ count]));
+}
+
+void TableRowNSImageCache::OnItemsRemoved(int start, int length) {
+ DCHECK_LE(start + length, static_cast<int>([icon_images_ count]));
+ for (int i = 0; i < length; ++i) {
+ [icon_images_ removePointerAtIndex:start]; // Values slide down.
+ }
+ DCHECK_EQ(model_->RowCount(),
+ static_cast<int>([icon_images_ count]));
+}
+
+NSImage* TableRowNSImageCache::GetImageForRow(int row) {
+ DCHECK_EQ(model_->RowCount(),
+ static_cast<int>([icon_images_ count]));
+ DCHECK_GE(row, 0);
+ DCHECK_LT(row, static_cast<int>([icon_images_ count]));
+ NSImage* image = static_cast<NSImage*>([icon_images_ pointerAtIndex:row]);
+ if (!image) {
+ const SkBitmap bitmap_icon =
+ model_->GetIcon(row);
+ // This means GetIcon() will get called until it returns a non-empty bitmap.
+ // Empty bitmaps are intentionally not cached.
+ if (!bitmap_icon.isNull()) {
+ image = gfx::SkBitmapToNSImage(bitmap_icon);
+ DCHECK(image);
+ [icon_images_ replacePointerAtIndex:row withPointer:image];
+ }
+ }
+ return image;
+}
+
diff --git a/chrome/browser/ui/cocoa/table_row_nsimage_cache_unittest.mm b/chrome/browser/ui/cocoa/table_row_nsimage_cache_unittest.mm
new file mode 100644
index 0000000..ecca71e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/table_row_nsimage_cache_unittest.mm
@@ -0,0 +1,200 @@
+// 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 "chrome/browser/ui/cocoa/table_row_nsimage_cache.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+namespace {
+
+class TestTable : public TableRowNSImageCache::Table {
+ public:
+
+ std::vector<SkBitmap>* rows() {
+ return &rows_;
+ }
+
+ // TableRowNSImageCache::Table overrides.
+ virtual int RowCount() const {
+ return rows_.size();
+ }
+ virtual SkBitmap GetIcon(int index) const {
+ return rows_[index];
+ }
+
+ private:
+ std::vector<SkBitmap> rows_;
+};
+
+SkBitmap MakeImage(int width, int height) {
+ SkBitmap image;
+ image.setConfig(SkBitmap::kARGB_8888_Config, width, height);
+ EXPECT_TRUE(image.allocPixels());
+ image.eraseRGB(255, 0, 0);
+ return image;
+}
+
+// Define this as a macro so that the original variable names can be used in
+// test output.
+#define COMPARE_SK_NS_IMG_SIZES(skia, cocoa) \
+ EXPECT_EQ(skia.width(), [cocoa size].width); \
+ EXPECT_EQ(skia.height(), [cocoa size].height);
+
+TEST(TableRowNSImageCacheTest, ModelChanged) {
+ TestTable table;
+ std::vector<SkBitmap>* rows = table.rows();
+ rows->push_back(MakeImage(10, 10));
+ rows->push_back(MakeImage(20, 20));
+ rows->push_back(MakeImage(30, 30));
+ TableRowNSImageCache cache(&table);
+
+ NSImage* image0 = cache.GetImageForRow(0);
+ NSImage* image1 = cache.GetImageForRow(1);
+ NSImage* image2 = cache.GetImageForRow(2);
+
+ COMPARE_SK_NS_IMG_SIZES(rows->at(0), image0);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(1), image1);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(2), image2);
+
+ rows->clear();
+
+ rows->push_back(MakeImage(15, 15));
+ rows->push_back(MakeImage(25, 25));
+ rows->push_back(MakeImage(35, 35));
+ rows->push_back(MakeImage(45, 45));
+
+ // Invalidate the entire model.
+ cache.OnModelChanged();
+
+ EXPECT_NE(image0, cache.GetImageForRow(0));
+ image0 = cache.GetImageForRow(0);
+
+ EXPECT_NE(image1, cache.GetImageForRow(1));
+ image1 = cache.GetImageForRow(1);
+
+ EXPECT_NE(image2, cache.GetImageForRow(2));
+ image2 = cache.GetImageForRow(2);
+
+ NSImage* image3 = cache.GetImageForRow(3);
+
+ COMPARE_SK_NS_IMG_SIZES(rows->at(0), image0);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(1), image1);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(2), image2);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(3), image3);
+}
+
+
+TEST(TableRowNSImageCacheTest, ItemsChanged) {
+ TestTable table;
+ std::vector<SkBitmap>* rows = table.rows();
+ rows->push_back(MakeImage(10, 10));
+ rows->push_back(MakeImage(20, 20));
+ rows->push_back(MakeImage(30, 30));
+ TableRowNSImageCache cache(&table);
+
+ NSImage* image0 = cache.GetImageForRow(0);
+ NSImage* image1 = cache.GetImageForRow(1);
+ NSImage* image2 = cache.GetImageForRow(2);
+
+ COMPARE_SK_NS_IMG_SIZES(rows->at(0), image0);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(1), image1);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(2), image2);
+
+ // Update the middle image.
+ (*rows)[1] = MakeImage(25, 25);
+ cache.OnItemsChanged(/* start=*/1, /* count=*/1);
+
+ // Make sure the other items remained the same.
+ EXPECT_EQ(image0, cache.GetImageForRow(0));
+ EXPECT_EQ(image2, cache.GetImageForRow(2));
+
+ image1 = cache.GetImageForRow(1);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(1), image1);
+
+ // Update more than one image.
+ (*rows)[0] = MakeImage(15, 15);
+ (*rows)[1] = MakeImage(27, 27);
+ EXPECT_EQ(3U, rows->size());
+ cache.OnItemsChanged(0, 2);
+
+ image0 = cache.GetImageForRow(0);
+ image1 = cache.GetImageForRow(1);
+
+ COMPARE_SK_NS_IMG_SIZES(rows->at(0), image0);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(1), image1);
+}
+
+
+TEST(TableRowNSImageCacheTest, ItemsAdded) {
+ TestTable table;
+ std::vector<SkBitmap>* rows = table.rows();
+ rows->push_back(MakeImage(10, 10));
+ rows->push_back(MakeImage(20, 20));
+ TableRowNSImageCache cache(&table);
+
+ NSImage* image0 = cache.GetImageForRow(0);
+ NSImage* image1 = cache.GetImageForRow(1);
+
+ COMPARE_SK_NS_IMG_SIZES(rows->at(0), image0);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(1), image1);
+
+ // Add an item to the end.
+ rows->push_back(MakeImage(30, 30));
+ cache.OnItemsAdded(2, 1);
+
+ // Make sure image 1 stayed the same.
+ EXPECT_EQ(image1, cache.GetImageForRow(1));
+ COMPARE_SK_NS_IMG_SIZES(rows->at(1), image1);
+
+ // Check that image 2 got added correctly.
+ NSImage* image2 = cache.GetImageForRow(2);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(2), image2);
+
+ // Add two items to the begging.
+ rows->insert(rows->begin(), MakeImage(7, 7));
+ rows->insert(rows->begin(), MakeImage(3, 3));
+ cache.OnItemsAdded(0, 2);
+
+ NSImage* image00 = cache.GetImageForRow(0);
+ NSImage* image01 = cache.GetImageForRow(1);
+
+ COMPARE_SK_NS_IMG_SIZES(rows->at(0), image00);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(1), image01);
+}
+
+
+TEST(TableRowNSImageCacheTest, ItemsRemoved) {
+ TestTable table;
+ std::vector<SkBitmap>* rows = table.rows();
+ rows->push_back(MakeImage(10, 10));
+ rows->push_back(MakeImage(20, 20));
+ rows->push_back(MakeImage(30, 30));
+ rows->push_back(MakeImage(40, 40));
+ rows->push_back(MakeImage(50, 50));
+ TableRowNSImageCache cache(&table);
+
+ NSImage* image0 = cache.GetImageForRow(0);
+ NSImage* image1 = cache.GetImageForRow(1);
+ NSImage* image2 = cache.GetImageForRow(2);
+ NSImage* image3 = cache.GetImageForRow(3);
+ NSImage* image4 = cache.GetImageForRow(4);
+
+ COMPARE_SK_NS_IMG_SIZES(rows->at(0), image0);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(1), image1);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(2), image2);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(3), image3);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(4), image4);
+
+ rows->erase(rows->begin() + 1, rows->begin() + 4); // [20x20, 50x50)
+ cache.OnItemsRemoved(1, 3);
+
+ image0 = cache.GetImageForRow(0);
+ image1 = cache.GetImageForRow(1);
+
+ COMPARE_SK_NS_IMG_SIZES(rows->at(0), image0);
+ COMPARE_SK_NS_IMG_SIZES(rows->at(1), image1);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/tabpose_window.h b/chrome/browser/ui/cocoa/tabpose_window.h
new file mode 100644
index 0000000..b798bcc
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tabpose_window.h
@@ -0,0 +1,94 @@
+// 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_UI_COCOA_TABPOSE_WINDOW_H_
+#define CHROME_BROWSER_UI_COCOA_TABPOSE_WINDOW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/mac/scoped_cftyperef.h"
+
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "base/scoped_vector.h"
+
+namespace tabpose {
+
+class Tile;
+class TileSet;
+
+}
+
+namespace tabpose {
+
+enum WindowState {
+ kFadingIn,
+ kFadedIn,
+ kFadingOut,
+};
+
+} // namespace tabpose
+
+class TabStripModel;
+class TabStripModelObserverBridge;
+
+// A TabposeWindow shows an overview of open tabs and lets the user select a new
+// active tab. The window blocks clicks on the tab strip and the download
+// shelf. Every open browser window has its own overlay, and they are
+// independent of each other.
+@interface TabposeWindow : NSWindow {
+ @private
+ tabpose::WindowState state_;
+
+ // The root layer added to the content view. Covers the whole window.
+ CALayer* rootLayer_; // weak
+
+ // The layer showing the background layer. Covers the whole visible area.
+ CALayer* bgLayer_; // weak
+
+ // The layer drawn behind the currently selected tile.
+ CALayer* selectionHighlight_; // weak
+
+ // Colors used by the layers.
+ base::mac::ScopedCFTypeRef<CGColorRef> gray_;
+ base::mac::ScopedCFTypeRef<CGColorRef> darkBlue_;
+
+ TabStripModel* tabStripModel_; // weak
+
+ // Stores all preview layers. The order in here matches the order in
+ // the tabstrip model.
+ scoped_nsobject<NSMutableArray> allThumbnailLayers_;
+
+ scoped_nsobject<NSMutableArray> allFaviconLayers_;
+ scoped_nsobject<NSMutableArray> allTitleLayers_;
+
+ // Manages the state of all layers.
+ scoped_ptr<tabpose::TileSet> tileSet_;
+
+ // The rectangle of the window that contains all layers. This is the
+ // rectangle occupied by |bgLayer_|.
+ NSRect containingRect_;
+
+ // Informs us of added/removed/updated tabs.
+ scoped_ptr<TabStripModelObserverBridge> tabStripModelObserverBridge_;
+}
+
+// Shows a TabposeWindow on top of |parent|, with |rect| being the active area.
+// If |slomo| is YES, then the appearance animation is shown in slow motion.
+// The window blocks all keyboard and mouse events and releases itself when
+// closed.
++ (id)openTabposeFor:(NSWindow*)parent
+ rect:(NSRect)rect
+ slomo:(BOOL)slomo
+ tabStripModel:(TabStripModel*)tabStripModel;
+@end
+
+@interface TabposeWindow (TestingAPI)
+- (void)selectTileAtIndexWithoutAnimation:(int)newIndex;
+- (NSUInteger)thumbnailLayerCount;
+- (int)selectedIndex;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_TABPOSE_WINDOW_H_
diff --git a/chrome/browser/ui/cocoa/tabpose_window.mm b/chrome/browser/ui/cocoa/tabpose_window.mm
new file mode 100644
index 0000000..47825f5
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tabpose_window.mm
@@ -0,0 +1,1437 @@
+// 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/ui/cocoa/tabpose_window.h"
+
+#import <QuartzCore/QuartzCore.h>
+
+#include "app/resource_bundle.h"
+#include "base/mac_util.h"
+#include "base/mac/scoped_cftyperef.h"
+#include "base/scoped_callback_factory.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/browser_process.h"
+#import "chrome/browser/debugger/devtools_window.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/renderer_host/backing_store_mac.h"
+#include "chrome/browser/renderer_host/render_view_host.h"
+#include "chrome/browser/renderer_host/render_widget_host_view_mac.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents_wrapper.h"
+#include "chrome/browser/tab_contents/thumbnail_generator.h"
+#include "chrome/browser/tab_contents_wrapper.h"
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/tab_strip_controller.h"
+#import "chrome/browser/ui/cocoa/tab_strip_model_observer_bridge.h"
+#include "chrome/common/pref_names.h"
+#include "grit/app_resources.h"
+#include "skia/ext/skia_utils_mac.h"
+#include "third_party/skia/include/utils/mac/SkCGUtils.h"
+
+const int kTopGradientHeight = 15;
+
+NSString* const kAnimationIdKey = @"AnimationId";
+NSString* const kAnimationIdFadeIn = @"FadeIn";
+NSString* const kAnimationIdFadeOut = @"FadeOut";
+
+const CGFloat kDefaultAnimationDuration = 0.25; // In seconds.
+const CGFloat kSlomoFactor = 4;
+const CGFloat kObserverChangeAnimationDuration = 0.75; // In seconds.
+
+// CAGradientLayer is 10.6-only -- roll our own.
+@interface DarkGradientLayer : CALayer
+- (void)drawInContext:(CGContextRef)context;
+@end
+
+@implementation DarkGradientLayer
+- (void)drawInContext:(CGContextRef)context {
+ base::mac::ScopedCFTypeRef<CGColorSpaceRef> grayColorSpace(
+ CGColorSpaceCreateWithName(kCGColorSpaceGenericGray));
+ CGFloat grays[] = { 0.277, 1.0, 0.39, 1.0 };
+ CGFloat locations[] = { 0, 1 };
+ base::mac::ScopedCFTypeRef<CGGradientRef> gradient(
+ CGGradientCreateWithColorComponents(
+ grayColorSpace.get(), grays, locations, arraysize(locations)));
+ CGPoint topLeft = CGPointMake(0.0, kTopGradientHeight);
+ CGContextDrawLinearGradient(context, gradient.get(), topLeft, CGPointZero, 0);
+}
+@end
+
+namespace tabpose {
+class ThumbnailLoader;
+}
+
+// A CALayer that draws a thumbnail for a TabContents object. The layer tries
+// to draw the TabContents's backing store directly if possible, and requests
+// a thumbnail bitmap from the TabContents's renderer process if not.
+@interface ThumbnailLayer : CALayer {
+ // The TabContents the thumbnail is for.
+ TabContents* contents_; // weak
+
+ // The size the thumbnail is drawn at when zoomed in.
+ NSSize fullSize_;
+
+ // Used to load a thumbnail, if required.
+ scoped_refptr<tabpose::ThumbnailLoader> loader_;
+
+ // If the backing store couldn't be used and a thumbnail was returned from a
+ // renderer process, it's stored in |thumbnail_|.
+ base::mac::ScopedCFTypeRef<CGImageRef> thumbnail_;
+
+ // True if the layer already sent a thumbnail request to a renderer.
+ BOOL didSendLoad_;
+}
+- (id)initWithTabContents:(TabContents*)contents fullSize:(NSSize)fullSize;
+- (void)drawInContext:(CGContextRef)context;
+- (void)setThumbnail:(const SkBitmap&)bitmap;
+@end
+
+namespace tabpose {
+
+// ThumbnailLoader talks to the renderer process to load a thumbnail of a given
+// RenderWidgetHost, and sends the thumbnail back to a ThumbnailLayer once it
+// comes back from the renderer.
+class ThumbnailLoader : public base::RefCountedThreadSafe<ThumbnailLoader> {
+ public:
+ ThumbnailLoader(gfx::Size size, RenderWidgetHost* rwh, ThumbnailLayer* layer)
+ : size_(size), rwh_(rwh), layer_(layer), factory_(this) {}
+
+ // Starts the fetch.
+ void LoadThumbnail();
+
+ private:
+ friend class base::RefCountedThreadSafe<ThumbnailLoader>;
+ ~ThumbnailLoader() {
+ ResetPaintingObserver();
+ }
+
+ void DidReceiveBitmap(const SkBitmap& bitmap) {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+ ResetPaintingObserver();
+ [layer_ setThumbnail:bitmap];
+ }
+
+ void ResetPaintingObserver() {
+ if (rwh_->painting_observer() != NULL) {
+ DCHECK(rwh_->painting_observer() ==
+ g_browser_process->GetThumbnailGenerator());
+ rwh_->set_painting_observer(NULL);
+ }
+ }
+
+ gfx::Size size_;
+ RenderWidgetHost* rwh_; // weak
+ ThumbnailLayer* layer_; // weak, owns us
+ base::ScopedCallbackFactory<ThumbnailLoader> factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(ThumbnailLoader);
+};
+
+void ThumbnailLoader::LoadThumbnail() {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+ ThumbnailGenerator* generator = g_browser_process->GetThumbnailGenerator();
+ if (!generator) // In unit tests.
+ return;
+
+ // As mentioned in ThumbnailLayer's -drawInContext:, it's sufficient to have
+ // thumbnails at the zoomed-out pixel size for all but the thumbnail the user
+ // clicks on in the end. But we don't don't which thumbnail that will be, so
+ // keep it simple and request full thumbnails for everything.
+ // TODO(thakis): Request smaller thumbnails for users with many tabs.
+ gfx::Size page_size(size_); // Logical size the renderer renders at.
+ gfx::Size pixel_size(size_); // Physical pixel size the image is rendered at.
+
+ DCHECK(rwh_->painting_observer() == NULL ||
+ rwh_->painting_observer() == generator);
+ rwh_->set_painting_observer(generator);
+
+ // Will send an IPC to the renderer on the IO thread.
+ generator->AskForSnapshot(
+ rwh_,
+ /*prefer_backing_store=*/false,
+ factory_.NewCallback(&ThumbnailLoader::DidReceiveBitmap),
+ page_size,
+ pixel_size);
+}
+
+} // namespace tabpose
+
+@implementation ThumbnailLayer
+
+- (id)initWithTabContents:(TabContents*)contents fullSize:(NSSize)fullSize {
+ CHECK(contents);
+ if ((self = [super init])) {
+ contents_ = contents;
+ fullSize_ = fullSize;
+ }
+ return self;
+}
+
+- (void)setTabContents:(TabContents*)contents {
+ contents_ = contents;
+}
+
+- (void)setThumbnail:(const SkBitmap&)bitmap {
+ // SkCreateCGImageRef() holds on to |bitmaps|'s memory, so this doesn't
+ // create a copy.
+ thumbnail_.reset(SkCreateCGImageRef(bitmap));
+ loader_ = NULL;
+ [self setNeedsDisplay];
+}
+
+- (int)topOffset {
+ int topOffset = 0;
+
+ // Medium term, we want to show thumbs of the actual info bar views, which
+ // means I need to create InfoBarControllers here. At that point, we can get
+ // the height from that controller. Until then, hardcode. :-/
+ const int kInfoBarHeight = 31;
+ topOffset += contents_->infobar_delegate_count() * kInfoBarHeight;
+
+ bool always_show_bookmark_bar =
+ contents_->profile()->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar);
+ bool has_detached_bookmark_bar =
+ contents_->ShouldShowBookmarkBar() && !always_show_bookmark_bar;
+ if (has_detached_bookmark_bar)
+ topOffset += bookmarks::kNTPBookmarkBarHeight;
+
+ return topOffset;
+}
+
+- (int)bottomOffset {
+ int bottomOffset = 0;
+ TabContents* devToolsContents =
+ DevToolsWindow::GetDevToolsContents(contents_);
+ if (devToolsContents && devToolsContents->render_view_host() &&
+ devToolsContents->render_view_host()->view()) {
+ // The devtool's size might not be up-to-date, but since its height doesn't
+ // change on window resize, and since most users don't use devtools, this is
+ // good enough.
+ bottomOffset +=
+ devToolsContents->render_view_host()->view()->GetViewBounds().height();
+ bottomOffset += 1; // :-( Divider line between web contents and devtools.
+ }
+ return bottomOffset;
+}
+
+- (void)drawBackingStore:(BackingStoreMac*)backing_store
+ inRect:(CGRect)destRect
+ context:(CGContextRef)context {
+ // TODO(thakis): Add a sublayer for each accelerated surface in the rwhv.
+ // Until then, accelerated layers (CoreAnimation NPAPI plugins, compositor)
+ // won't show up in tabpose.
+ if (backing_store->cg_layer()) {
+ CGContextDrawLayerInRect(context, destRect, backing_store->cg_layer());
+ } else {
+ base::mac::ScopedCFTypeRef<CGImageRef> image(
+ CGBitmapContextCreateImage(backing_store->cg_bitmap()));
+ CGContextDrawImage(context, destRect, image);
+ }
+}
+
+- (void)drawInContext:(CGContextRef)context {
+ RenderWidgetHost* rwh = contents_->render_view_host();
+ // NULL if renderer crashed.
+ RenderWidgetHostView* rwhv = rwh ? rwh->view() : NULL;
+ if (!rwhv) {
+ // TODO(thakis): Maybe draw a sad tab layer?
+ [super drawInContext:context];
+ return;
+ }
+
+ // The size of the TabContent's RenderWidgetHost might not fit to the
+ // current browser window at all, for example if the window was resized while
+ // this TabContents object was not an active tab.
+ // Compute the required size ourselves. Leave room for eventual infobars and
+ // a detached bookmarks bar on the top, and for the devtools on the bottom.
+ // Download shelf is not included in the |fullSize| rect, so no need to
+ // correct for it here.
+ // TODO(thakis): This is not resolution-independent.
+ int topOffset = [self topOffset];
+ int bottomOffset = [self bottomOffset];
+ gfx::Size desiredThumbSize(fullSize_.width,
+ fullSize_.height - topOffset - bottomOffset);
+
+ // We need to ask the renderer for a thumbnail if
+ // a) there's no backing store or
+ // b) the backing store's size doesn't match our required size and
+ // c) we didn't already send a thumbnail request to the renderer.
+ BackingStoreMac* backing_store =
+ (BackingStoreMac*)rwh->GetBackingStore(/*force_create=*/false);
+ bool draw_backing_store =
+ backing_store && backing_store->size() == desiredThumbSize;
+
+ // Next weirdness: The destination rect. If the layer is |fullSize_| big, the
+ // destination rect is (0, bottomOffset), (fullSize_.width, topOffset). But we
+ // might be amidst an animation, so interpolate that rect.
+ CGRect destRect = [self bounds];
+ CGFloat scale = destRect.size.width / fullSize_.width;
+ destRect.origin.y += bottomOffset * scale;
+ destRect.size.height -= (bottomOffset + topOffset) * scale;
+
+ // TODO(thakis): Draw infobars, detached bookmark bar as well.
+
+ // If we haven't already, sent a thumbnail request to the renderer.
+ if (!draw_backing_store && !didSendLoad_) {
+ // Either the tab was never visible, or its backing store got evicted, or
+ // the size of the backing store is wrong.
+
+ // We only need a thumbnail the size of the zoomed-out layer for all
+ // layers except the one the user clicks on. But since we can't know which
+ // layer that is, request full-resolution layers for all tabs. This is
+ // simple and seems to work in practice.
+ loader_ = new tabpose::ThumbnailLoader(desiredThumbSize, rwh, self);
+ loader_->LoadThumbnail();
+ didSendLoad_ = YES;
+
+ // Fill with bg color.
+ [super drawInContext:context];
+ }
+
+ if (draw_backing_store) {
+ // Backing store 'cache' hit!
+ [self drawBackingStore:backing_store inRect:destRect context:context];
+ } else if (thumbnail_) {
+ // No cache hit, but the renderer returned a thumbnail to us.
+ CGContextDrawImage(context, destRect, thumbnail_.get());
+ }
+}
+
+@end
+
+namespace {
+
+class ScopedCAActionDisabler {
+ public:
+ ScopedCAActionDisabler() {
+ [CATransaction begin];
+ [CATransaction setValue:[NSNumber numberWithBool:YES]
+ forKey:kCATransactionDisableActions];
+ }
+
+ ~ScopedCAActionDisabler() {
+ [CATransaction commit];
+ }
+};
+
+class ScopedCAActionSetDuration {
+ public:
+ explicit ScopedCAActionSetDuration(CGFloat duration) {
+ [CATransaction begin];
+ [CATransaction setValue:[NSNumber numberWithFloat:duration]
+ forKey:kCATransactionAnimationDuration];
+ }
+
+ ~ScopedCAActionSetDuration() {
+ [CATransaction commit];
+ }
+};
+
+} // namespace
+
+// Given the number |n| of tiles with a desired aspect ratio of |a| and a
+// desired distance |dx|, |dy| between tiles, returns how many tiles fit
+// vertically into a rectangle with the dimensions |w_c|, |h_c|. This returns
+// an exact solution, which is usually a fractional number.
+static float FitNRectsWithAspectIntoBoundingSizeWithConstantPadding(
+ int n, double a, int w_c, int h_c, int dx, int dy) {
+ // We want to have the small rects have the same aspect ratio a as a full
+ // tab. Let w, h be the size of a small rect, and w_c, h_c the size of the
+ // container. dx, dy are the distances between small rects in x, y direction.
+
+ // Geometry yields:
+ // w_c = nx * (w + dx) - dx <=> w = (w_c + d_x) / nx - d_x
+ // h_c = ny * (h + dy) - dy <=> h = (h_c + d_y) / ny - d_t
+ // Plugging this into
+ // a := tab_width / tab_height = w / h
+ // yields
+ // a = ((w_c - (nx - 1)*d_x)*ny) / (nx*(h_c - (ny - 1)*d_y))
+ // Plugging in nx = n/ny and pen and paper (or wolfram alpha:
+ // http://www.wolframalpha.com/input/?i=(-sqrt((d+n-a+f+n)^2-4+(a+f%2Ba+h)+(-d+n-n+w))%2Ba+f+n-d+n)/(2+a+(f%2Bh)) , (solution for nx)
+ // http://www.wolframalpha.com/input/?i=+(-sqrt((a+f+n-d+n)^2-4+(d%2Bw)+(-a+f+n-a+h+n))-a+f+n%2Bd+n)/(2+(d%2Bw)) , (solution for ny)
+ // ) gives us nx and ny (but the wrong root -- s/-sqrt(FOO)/sqrt(FOO)/.
+
+ // This function returns ny.
+ return (sqrt(pow(n * (a * dy - dx), 2) +
+ 4 * n * a * (dx + w_c) * (dy + h_c)) -
+ n * (a * dy - dx))
+ /
+ (2 * (dx + w_c));
+}
+
+namespace tabpose {
+
+// A tile is what is shown for a single tab in tabpose mode. It consists of a
+// title, favicon, thumbnail image, and pre- and postanimation rects.
+class Tile {
+ public:
+ Tile() {}
+
+ // Returns the rectangle this thumbnail is at at the beginning of the zoom-in
+ // animation. |tile| is the rectangle that's covering the whole tab area when
+ // the animation starts.
+ NSRect GetStartRectRelativeTo(const Tile& tile) const;
+ NSRect thumb_rect() const { return thumb_rect_; }
+
+ NSRect favicon_rect() const { return favicon_rect_; }
+ SkBitmap favicon() const;
+
+ // This changes |title_rect| and |favicon_rect| such that the favicon is on
+ // the font's baseline and that the minimum distance between thumb rect and
+ // favicon and title rects doesn't change.
+ // The view code
+ // 1. queries desired font size by calling |title_font_size()|
+ // 2. loads that font
+ // 3. calls |set_font_metrics()| which updates the title rect
+ // 4. receives the title rect and puts the title on it with the font from 2.
+ void set_font_metrics(CGFloat ascender, CGFloat descender);
+ CGFloat title_font_size() const { return title_font_size_; }
+
+ NSRect title_rect() const { return title_rect_; }
+
+ // Returns an unelided title. The view logic is responsible for eliding.
+ const string16& title() const { return contents_->GetTitle(); }
+
+ TabContents* tab_contents() const { return contents_; }
+ void set_tab_contents(TabContents* new_contents) { contents_ = new_contents; }
+
+ private:
+ friend class TileSet;
+
+ // The thumb rect includes infobars, detached thumbnail bar, web contents,
+ // and devtools.
+ NSRect thumb_rect_;
+ NSRect start_thumb_rect_;
+
+ NSRect favicon_rect_;
+
+ CGFloat title_font_size_;
+ NSRect title_rect_;
+
+ TabContents* contents_; // weak
+
+ DISALLOW_COPY_AND_ASSIGN(Tile);
+};
+
+NSRect Tile::GetStartRectRelativeTo(const Tile& tile) const {
+ NSRect rect = start_thumb_rect_;
+ rect.origin.x -= tile.start_thumb_rect_.origin.x;
+ rect.origin.y -= tile.start_thumb_rect_.origin.y;
+ return rect;
+}
+
+SkBitmap Tile::favicon() const {
+ if (contents_->is_app()) {
+ SkBitmap* icon = contents_->GetExtensionAppIcon();
+ if (icon)
+ return *icon;
+ }
+ return contents_->GetFavIcon();
+}
+
+// Changes |title_rect| and |favicon_rect| such that the favicon is on the
+// font's baseline and that the minimum distance between thumb rect and
+// favicon and title rects doesn't change.
+void Tile::set_font_metrics(CGFloat ascender, CGFloat descender) {
+ title_rect_.origin.y -= ascender + descender - NSHeight(title_rect_);
+ title_rect_.size.height = ascender + descender;
+
+ if (NSHeight(favicon_rect_) < ascender) {
+ // Move favicon down.
+ favicon_rect_.origin.y = title_rect_.origin.y + descender;
+ } else {
+ // Move title down.
+ title_rect_.origin.y = favicon_rect_.origin.y - descender;
+ }
+}
+
+// A tileset is responsible for owning and laying out all |Tile|s shown in a
+// tabpose window.
+class TileSet {
+ public:
+ TileSet() {}
+
+ // Fills in |tiles_|.
+ void Build(TabStripModel* source_model);
+
+ // Computes coordinates for |tiles_|.
+ void Layout(NSRect containing_rect);
+
+ int selected_index() const { return selected_index_; }
+ void set_selected_index(int index);
+
+ const Tile& selected_tile() const { return *tiles_[selected_index()]; }
+ Tile& tile_at(int index) { return *tiles_[index]; }
+ const Tile& tile_at(int index) const { return *tiles_[index]; }
+
+ // These return which index needs to be selected when the user presses
+ // up, down, left, or right respectively.
+ int up_index() const;
+ int down_index() const;
+ int left_index() const;
+ int right_index() const;
+
+ // These return which index needs to be selected on tab / shift-tab.
+ int next_index() const;
+ int previous_index() const;
+
+ // Inserts a new Tile object containing |contents| at |index|. Does no
+ // relayout.
+ void InsertTileAt(int index, TabContents* contents);
+
+ // Removes the Tile object at |index|. Does no relayout.
+ void RemoveTileAt(int index);
+
+ // Moves the Tile object at |from_index| to |to_index|. Since this doesn't
+ // change the number of tiles, relayout can be done just by swapping the
+ // tile rectangles in the index interval [from_index, to_index], so this does
+ // layout.
+ void MoveTileFromTo(int from_index, int to_index);
+
+ private:
+ int count_x() const {
+ return ceilf(tiles_.size() / static_cast<float>(count_y_));
+ }
+ int count_y() const {
+ return count_y_;
+ }
+ int last_row_count_x() const {
+ return tiles_.size() - count_x() * (count_y() - 1);
+ }
+ int tiles_in_row(int row) const {
+ return row != count_y() - 1 ? count_x() : last_row_count_x();
+ }
+ void index_to_tile_xy(int index, int* tile_x, int* tile_y) const {
+ *tile_x = index % count_x();
+ *tile_y = index / count_x();
+ }
+ int tile_xy_to_index(int tile_x, int tile_y) const {
+ return tile_y * count_x() + tile_x;
+ }
+
+ ScopedVector<Tile> tiles_;
+ int selected_index_;
+ int count_y_;
+
+ DISALLOW_COPY_AND_ASSIGN(TileSet);
+};
+
+void TileSet::Build(TabStripModel* source_model) {
+ selected_index_ = source_model->selected_index();
+ tiles_.resize(source_model->count());
+ for (size_t i = 0; i < tiles_.size(); ++i) {
+ tiles_[i] = new Tile;
+ tiles_[i]->contents_ = source_model->GetTabContentsAt(i)->tab_contents();
+ }
+}
+
+void TileSet::Layout(NSRect containing_rect) {
+ int tile_count = tiles_.size();
+ if (tile_count == 0) // Happens e.g. during test shutdown.
+ return;
+
+ // Room around the tiles insde of |containing_rect|.
+ const int kSmallPaddingTop = 30;
+ const int kSmallPaddingLeft = 30;
+ const int kSmallPaddingRight = 30;
+ const int kSmallPaddingBottom = 30;
+
+ // Favicon / title area.
+ const int kThumbTitlePaddingY = 6;
+ const int kFaviconSize = 16;
+ const int kTitleHeight = 14; // Font size.
+ const int kTitleExtraHeight = kThumbTitlePaddingY + kTitleHeight;
+ const int kFaviconExtraHeight = kThumbTitlePaddingY + kFaviconSize;
+ const int kFaviconTitleDistanceX = 6;
+ const int kFooterExtraHeight =
+ std::max(kFaviconExtraHeight, kTitleExtraHeight);
+
+ // Room between the tiles.
+ const int kSmallPaddingX = 15;
+ const int kSmallPaddingY = kFooterExtraHeight;
+
+ // Aspect ratio of the containing rect.
+ CGFloat aspect = NSWidth(containing_rect) / NSHeight(containing_rect);
+
+ // Room left in container after the outer padding is removed.
+ double container_width =
+ NSWidth(containing_rect) - kSmallPaddingLeft - kSmallPaddingRight;
+ double container_height =
+ NSHeight(containing_rect) - kSmallPaddingTop - kSmallPaddingBottom;
+
+ // The tricky part is figuring out the size of a tab thumbnail, or since the
+ // size of the containing rect is known, the number of tiles in x and y
+ // direction.
+ // Given are the size of the containing rect, and the number of thumbnails
+ // that need to fit into that rect. The aspect ratio of the thumbnails needs
+ // to be the same as that of |containing_rect|, else they will look distorted.
+ // The thumbnails need to be distributed such that
+ // |count_x * count_y >= tile_count|, and such that wasted space is minimized.
+ // See the comments in
+ // |FitNRectsWithAspectIntoBoundingSizeWithConstantPadding()| for a more
+ // detailed discussion.
+ // TODO(thakis): It might be good enough to choose |count_x| and |count_y|
+ // such that count_x / count_y is roughly equal to |aspect|?
+ double fny = FitNRectsWithAspectIntoBoundingSizeWithConstantPadding(
+ tile_count, aspect,
+ container_width, container_height - kFooterExtraHeight,
+ kSmallPaddingX, kSmallPaddingY + kFooterExtraHeight);
+ count_y_ = roundf(fny);
+
+ // Now that |count_x()| and |count_y_| are known, it's straightforward to
+ // compute thumbnail width/height. See comment in
+ // |FitNRectsWithAspectIntoBoundingSizeWithConstantPadding| for the derivation
+ // of these two formulas.
+ int small_width =
+ floor((container_width + kSmallPaddingX) / static_cast<float>(count_x()) -
+ kSmallPaddingX);
+ int small_height =
+ floor((container_height + kSmallPaddingY) / static_cast<float>(count_y_) -
+ (kSmallPaddingY + kFooterExtraHeight));
+
+ // |small_width / small_height| has only roughly an aspect ratio of |aspect|.
+ // Shrink the thumbnail rect to make the aspect ratio fit exactly, and add
+ // the extra space won by shrinking to the outer padding.
+ int smallExtraPaddingLeft = 0;
+ int smallExtraPaddingTop = 0;
+ if (aspect > small_width/static_cast<float>(small_height)) {
+ small_height = small_width / aspect;
+ CGFloat all_tiles_height =
+ (small_height + kSmallPaddingY + kFooterExtraHeight) * count_y() -
+ (kSmallPaddingY + kFooterExtraHeight);
+ smallExtraPaddingTop = (container_height - all_tiles_height)/2;
+ } else {
+ small_width = small_height * aspect;
+ CGFloat all_tiles_width =
+ (small_width + kSmallPaddingX) * count_x() - kSmallPaddingX;
+ smallExtraPaddingLeft = (container_width - all_tiles_width)/2;
+ }
+
+ // Compute inter-tile padding in the zoomed-out view.
+ CGFloat scale_small_to_big =
+ NSWidth(containing_rect) / static_cast<float>(small_width);
+ CGFloat big_padding_x = kSmallPaddingX * scale_small_to_big;
+ CGFloat big_padding_y =
+ (kSmallPaddingY + kFooterExtraHeight) * scale_small_to_big;
+
+ // Now all dimensions are known. Lay out all tiles on a regular grid:
+ // X X X X
+ // X X X X
+ // X X
+ for (int row = 0, i = 0; i < tile_count; ++row) {
+ for (int col = 0; col < count_x() && i < tile_count; ++col, ++i) {
+ // Compute the smalled, zoomed-out thumbnail rect.
+ tiles_[i]->thumb_rect_.size = NSMakeSize(small_width, small_height);
+
+ int small_x = col * (small_width + kSmallPaddingX) +
+ kSmallPaddingLeft + smallExtraPaddingLeft;
+ int small_y = row * (small_height + kSmallPaddingY + kFooterExtraHeight) +
+ kSmallPaddingTop + smallExtraPaddingTop;
+
+ tiles_[i]->thumb_rect_.origin = NSMakePoint(
+ small_x, NSHeight(containing_rect) - small_y - small_height);
+
+ tiles_[i]->favicon_rect_.size = NSMakeSize(kFaviconSize, kFaviconSize);
+ tiles_[i]->favicon_rect_.origin = NSMakePoint(
+ small_x,
+ NSHeight(containing_rect) -
+ (small_y + small_height + kFaviconExtraHeight));
+
+ // Align lower left corner of title rect with lower left corner of favicon
+ // for now. The final position is computed later by
+ // |Tile::set_font_metrics()|.
+ tiles_[i]->title_font_size_ = kTitleHeight;
+ tiles_[i]->title_rect_.origin = NSMakePoint(
+ NSMaxX(tiles_[i]->favicon_rect()) + kFaviconTitleDistanceX,
+ NSMinY(tiles_[i]->favicon_rect()));
+ tiles_[i]->title_rect_.size = NSMakeSize(
+ small_width -
+ NSWidth(tiles_[i]->favicon_rect()) - kFaviconTitleDistanceX,
+ kTitleHeight);
+
+ // Compute the big, pre-zoom thumbnail rect.
+ tiles_[i]->start_thumb_rect_.size = containing_rect.size;
+
+ int big_x = col * (NSWidth(containing_rect) + big_padding_x);
+ int big_y = row * (NSHeight(containing_rect) + big_padding_y);
+ tiles_[i]->start_thumb_rect_.origin = NSMakePoint(big_x, -big_y);
+ }
+ }
+
+ // Go through last row and center it:
+ // X X X X
+ // X X X X
+ // X X
+ int last_row_empty_tiles_x = count_x() - last_row_count_x();
+ CGFloat small_last_row_shift_x =
+ last_row_empty_tiles_x * (small_width + kSmallPaddingX) / 2;
+ CGFloat big_last_row_shift_x =
+ last_row_empty_tiles_x * (NSWidth(containing_rect) + big_padding_x) / 2;
+ for (int i = tile_count - last_row_count_x(); i < tile_count; ++i) {
+ tiles_[i]->thumb_rect_.origin.x += small_last_row_shift_x;
+ tiles_[i]->start_thumb_rect_.origin.x += big_last_row_shift_x;
+ tiles_[i]->favicon_rect_.origin.x += small_last_row_shift_x;
+ tiles_[i]->title_rect_.origin.x += small_last_row_shift_x;
+ }
+}
+
+void TileSet::set_selected_index(int index) {
+ CHECK_GE(index, 0);
+ CHECK_LT(index, static_cast<int>(tiles_.size()));
+ selected_index_ = index;
+}
+
+// Given a |value| in [0, from_scale), map it into [0, to_scale) such that:
+// * [0, from_scale) ends up in the middle of [0, to_scale) if the latter is
+// a bigger range
+// * The middle of [0, from_scale) is mapped to [0, to_scale), and the parts
+// of the former that don't fit are mapped to 0 and to_scale - respectively
+// if the former is a bigger range.
+static int rescale(int value, int from_scale, int to_scale) {
+ int left = (to_scale - from_scale) / 2;
+ int result = value + left;
+ if (result < 0)
+ return 0;
+ if (result >= to_scale)
+ return to_scale - 1;
+ return result;
+}
+
+int TileSet::up_index() const {
+ int tile_x, tile_y;
+ index_to_tile_xy(selected_index(), &tile_x, &tile_y);
+ tile_y -= 1;
+ if (tile_y == count_y() - 2) {
+ // Transition from last row to second-to-last row.
+ tile_x = rescale(tile_x, last_row_count_x(), count_x());
+ } else if (tile_y < 0) {
+ // Transition from first row to last row.
+ tile_x = rescale(tile_x, count_x(), last_row_count_x());
+ tile_y = count_y() - 1;
+ }
+ return tile_xy_to_index(tile_x, tile_y);
+}
+
+int TileSet::down_index() const {
+ int tile_x, tile_y;
+ index_to_tile_xy(selected_index(), &tile_x, &tile_y);
+ tile_y += 1;
+ if (tile_y == count_y() - 1) {
+ // Transition from second-to-last row to last row.
+ tile_x = rescale(tile_x, count_x(), last_row_count_x());
+ } else if (tile_y >= count_y()) {
+ // Transition from last row to first row.
+ tile_x = rescale(tile_x, last_row_count_x(), count_x());
+ tile_y = 0;
+ }
+ return tile_xy_to_index(tile_x, tile_y);
+}
+
+int TileSet::left_index() const {
+ int tile_x, tile_y;
+ index_to_tile_xy(selected_index(), &tile_x, &tile_y);
+ tile_x -= 1;
+ if (tile_x < 0)
+ tile_x = tiles_in_row(tile_y) - 1;
+ return tile_xy_to_index(tile_x, tile_y);
+}
+
+int TileSet::right_index() const {
+ int tile_x, tile_y;
+ index_to_tile_xy(selected_index(), &tile_x, &tile_y);
+ tile_x += 1;
+ if (tile_x >= tiles_in_row(tile_y))
+ tile_x = 0;
+ return tile_xy_to_index(tile_x, tile_y);
+}
+
+int TileSet::next_index() const {
+ int new_index = selected_index() + 1;
+ if (new_index >= static_cast<int>(tiles_.size()))
+ new_index = 0;
+ return new_index;
+}
+
+int TileSet::previous_index() const {
+ int new_index = selected_index() - 1;
+ if (new_index < 0)
+ new_index = tiles_.size() - 1;
+ return new_index;
+}
+
+void TileSet::InsertTileAt(int index, TabContents* contents) {
+ tiles_.insert(tiles_.begin() + index, new Tile);
+ tiles_[index]->contents_ = contents;
+}
+
+void TileSet::RemoveTileAt(int index) {
+ tiles_.erase(tiles_.begin() + index);
+}
+
+// Moves the Tile object at |from_index| to |to_index|. Also updates rectangles
+// so that the tiles stay in a left-to-right, top-to-bottom layout when walked
+// in sequential order.
+void TileSet::MoveTileFromTo(int from_index, int to_index) {
+ NSRect thumb = tiles_[from_index]->thumb_rect_;
+ NSRect start_thumb = tiles_[from_index]->start_thumb_rect_;
+ NSRect favicon = tiles_[from_index]->favicon_rect_;
+ NSRect title = tiles_[from_index]->title_rect_;
+
+ scoped_ptr<Tile> tile(tiles_[from_index]);
+ tiles_.weak_erase(tiles_.begin() + from_index);
+ tiles_.insert(tiles_.begin() + to_index, tile.release());
+
+ int step = from_index < to_index ? -1 : 1;
+ for (int i = to_index; (i - from_index) * step < 0; i += step) {
+ tiles_[i]->thumb_rect_ = tiles_[i + step]->thumb_rect_;
+ tiles_[i]->start_thumb_rect_ = tiles_[i + step]->start_thumb_rect_;
+ tiles_[i]->favicon_rect_ = tiles_[i + step]->favicon_rect_;
+ tiles_[i]->title_rect_ = tiles_[i + step]->title_rect_;
+ }
+ tiles_[from_index]->thumb_rect_ = thumb;
+ tiles_[from_index]->start_thumb_rect_ = start_thumb;
+ tiles_[from_index]->favicon_rect_ = favicon;
+ tiles_[from_index]->title_rect_ = title;
+}
+
+} // namespace tabpose
+
+void AnimateCALayerFrameFromTo(
+ CALayer* layer, const NSRect& from, const NSRect& to,
+ NSTimeInterval duration, id boundsAnimationDelegate) {
+ // http://developer.apple.com/mac/library/qa/qa2008/qa1620.html
+ CABasicAnimation* animation;
+
+ animation = [CABasicAnimation animationWithKeyPath:@"bounds"];
+ animation.fromValue = [NSValue valueWithRect:from];
+ animation.toValue = [NSValue valueWithRect:to];
+ animation.duration = duration;
+ animation.timingFunction =
+ [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
+ animation.delegate = boundsAnimationDelegate;
+
+ // Update the layer's bounds so the layer doesn't snap back when the animation
+ // completes.
+ layer.bounds = NSRectToCGRect(to);
+
+ // Add the animation, overriding the implicit animation.
+ [layer addAnimation:animation forKey:@"bounds"];
+
+ // Prepare the animation from the current position to the new position.
+ NSPoint opoint = from.origin;
+ NSPoint point = to.origin;
+
+ // Adapt to anchorPoint.
+ opoint.x += NSWidth(from) * layer.anchorPoint.x;
+ opoint.y += NSHeight(from) * layer.anchorPoint.y;
+ point.x += NSWidth(to) * layer.anchorPoint.x;
+ point.y += NSHeight(to) * layer.anchorPoint.y;
+
+ animation = [CABasicAnimation animationWithKeyPath:@"position"];
+ animation.fromValue = [NSValue valueWithPoint:opoint];
+ animation.toValue = [NSValue valueWithPoint:point];
+ animation.duration = duration;
+ animation.timingFunction =
+ [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
+
+ // Update the layer's position so that the layer doesn't snap back when the
+ // animation completes.
+ layer.position = NSPointToCGPoint(point);
+
+ // Add the animation, overriding the implicit animation.
+ [layer addAnimation:animation forKey:@"position"];
+}
+
+@interface TabposeWindow (Private)
+- (id)initForWindow:(NSWindow*)parent
+ rect:(NSRect)rect
+ slomo:(BOOL)slomo
+ tabStripModel:(TabStripModel*)tabStripModel;
+- (void)setUpLayersInSlomo:(BOOL)slomo;
+- (void)fadeAway:(BOOL)slomo;
+- (void)selectTileAtIndex:(int)newIndex;
+@end
+
+@implementation TabposeWindow
+
++ (id)openTabposeFor:(NSWindow*)parent
+ rect:(NSRect)rect
+ slomo:(BOOL)slomo
+ tabStripModel:(TabStripModel*)tabStripModel {
+ // Releases itself when closed.
+ return [[TabposeWindow alloc]
+ initForWindow:parent rect:rect slomo:slomo tabStripModel:tabStripModel];
+}
+
+- (id)initForWindow:(NSWindow*)parent
+ rect:(NSRect)rect
+ slomo:(BOOL)slomo
+ tabStripModel:(TabStripModel*)tabStripModel {
+ NSRect frame = [parent frame];
+ if ((self = [super initWithContentRect:frame
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:NO])) {
+ containingRect_ = rect;
+ tabStripModel_ = tabStripModel;
+ state_ = tabpose::kFadingIn;
+ tileSet_.reset(new tabpose::TileSet);
+ tabStripModelObserverBridge_.reset(
+ new TabStripModelObserverBridge(tabStripModel_, self));
+ [self setReleasedWhenClosed:YES];
+ [self setOpaque:NO];
+ [self setBackgroundColor:[NSColor clearColor]];
+ [self setUpLayersInSlomo:slomo];
+ [self setAcceptsMouseMovedEvents:YES];
+ [parent addChildWindow:self ordered:NSWindowAbove];
+ [self makeKeyAndOrderFront:self];
+ }
+ return self;
+}
+
+- (CALayer*)selectedLayer {
+ return [allThumbnailLayers_ objectAtIndex:tileSet_->selected_index()];
+}
+
+- (void)selectTileAtIndex:(int)newIndex {
+ const tabpose::Tile& tile = tileSet_->tile_at(newIndex);
+ selectionHighlight_.frame =
+ NSRectToCGRect(NSInsetRect(tile.thumb_rect(), -5, -5));
+ tileSet_->set_selected_index(newIndex);
+}
+
+- (void)selectTileAtIndexWithoutAnimation:(int)newIndex {
+ ScopedCAActionDisabler disabler;
+ [self selectTileAtIndex:newIndex];
+}
+
+- (void)addLayersForTile:(tabpose::Tile&)tile
+ showZoom:(BOOL)showZoom
+ slomo:(BOOL)slomo
+ animationDelegate:(id)animationDelegate {
+ scoped_nsobject<CALayer> layer([[ThumbnailLayer alloc]
+ initWithTabContents:tile.tab_contents()
+ fullSize:tile.GetStartRectRelativeTo(
+ tileSet_->selected_tile()).size]);
+ [layer setNeedsDisplay];
+
+ // Background color as placeholder for now.
+ layer.get().backgroundColor = CGColorGetConstantColor(kCGColorWhite);
+ if (showZoom) {
+ AnimateCALayerFrameFromTo(
+ layer,
+ tile.GetStartRectRelativeTo(tileSet_->selected_tile()),
+ tile.thumb_rect(),
+ kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1),
+ animationDelegate);
+ } else {
+ layer.get().frame = NSRectToCGRect(tile.thumb_rect());
+ }
+
+ layer.get().shadowRadius = 10;
+ layer.get().shadowOffset = CGSizeMake(0, -10);
+ if (state_ == tabpose::kFadedIn)
+ layer.get().shadowOpacity = 0.5;
+
+ [bgLayer_ addSublayer:layer];
+ [allThumbnailLayers_ addObject:layer];
+
+ // Favicon and title.
+ NSFont* font = [NSFont systemFontOfSize:tile.title_font_size()];
+ tile.set_font_metrics([font ascender], -[font descender]);
+
+ NSImage* nsFavicon = gfx::SkBitmapToNSImage(tile.favicon());
+ // Either we don't have a valid favicon or there was some issue converting
+ // it from an SkBitmap. Either way, just show the default.
+ if (!nsFavicon) {
+ NSImage* defaultFavIcon =
+ ResourceBundle::GetSharedInstance().GetNativeImageNamed(
+ IDR_DEFAULT_FAVICON);
+ nsFavicon = defaultFavIcon;
+ }
+ base::mac::ScopedCFTypeRef<CGImageRef> favicon(
+ mac_util::CopyNSImageToCGImage(nsFavicon));
+
+ CALayer* faviconLayer = [CALayer layer];
+ faviconLayer.frame = NSRectToCGRect(tile.favicon_rect());
+ faviconLayer.contents = (id)favicon.get();
+ faviconLayer.zPosition = 1; // On top of the thumb shadow.
+ if (state_ == tabpose::kFadingIn)
+ faviconLayer.hidden = YES;
+ [bgLayer_ addSublayer:faviconLayer];
+ [allFaviconLayers_ addObject:faviconLayer];
+
+ CATextLayer* titleLayer = [CATextLayer layer];
+ titleLayer.frame = NSRectToCGRect(tile.title_rect());
+ titleLayer.string = base::SysUTF16ToNSString(tile.title());
+ titleLayer.fontSize = [font pointSize];
+ titleLayer.truncationMode = kCATruncationEnd;
+ titleLayer.font = font;
+ titleLayer.zPosition = 1; // On top of the thumb shadow.
+ if (state_ == tabpose::kFadingIn)
+ titleLayer.hidden = YES;
+ [bgLayer_ addSublayer:titleLayer];
+ [allTitleLayers_ addObject:titleLayer];
+}
+
+- (void)setUpLayersInSlomo:(BOOL)slomo {
+ // Root layer -- covers whole window.
+ rootLayer_ = [CALayer layer];
+
+ // In a block so that the layers don't fade in.
+ {
+ ScopedCAActionDisabler disabler;
+ // Background layer -- the visible part of the window.
+ gray_.reset(CGColorCreateGenericGray(0.39, 1.0));
+ bgLayer_ = [CALayer layer];
+ bgLayer_.backgroundColor = gray_;
+ bgLayer_.frame = NSRectToCGRect(containingRect_);
+ bgLayer_.masksToBounds = YES;
+ [rootLayer_ addSublayer:bgLayer_];
+
+ // Selection highlight layer.
+ darkBlue_.reset(CGColorCreateGenericRGB(0.25, 0.34, 0.86, 1.0));
+ selectionHighlight_ = [CALayer layer];
+ selectionHighlight_.backgroundColor = darkBlue_;
+ selectionHighlight_.cornerRadius = 5.0;
+ selectionHighlight_.zPosition = -1; // Behind other layers.
+ selectionHighlight_.hidden = YES;
+ [bgLayer_ addSublayer:selectionHighlight_];
+
+ // Top gradient.
+ CALayer* gradientLayer = [DarkGradientLayer layer];
+ gradientLayer.frame = CGRectMake(
+ 0,
+ NSHeight(containingRect_) - kTopGradientHeight,
+ NSWidth(containingRect_),
+ kTopGradientHeight);
+ [gradientLayer setNeedsDisplay]; // Draw once.
+ [bgLayer_ addSublayer:gradientLayer];
+ }
+
+ // Layers for the tab thumbnails.
+ tileSet_->Build(tabStripModel_);
+ tileSet_->Layout(containingRect_);
+ allThumbnailLayers_.reset(
+ [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]);
+ allFaviconLayers_.reset(
+ [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]);
+ allTitleLayers_.reset(
+ [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]);
+
+ for (int i = 0; i < tabStripModel_->count(); ++i) {
+ // Add a delegate to one of the animations to get a notification once the
+ // animations are done.
+ [self addLayersForTile:tileSet_->tile_at(i)
+ showZoom:YES
+ slomo:slomo
+ animationDelegate:i == tileSet_->selected_index() ? self : nil];
+ if (i == tileSet_->selected_index()) {
+ CALayer* layer = [allThumbnailLayers_ objectAtIndex:i];
+ CAAnimation* animation = [layer animationForKey:@"bounds"];
+ DCHECK(animation);
+ [animation setValue:kAnimationIdFadeIn forKey:kAnimationIdKey];
+ }
+ }
+ [self selectTileAtIndexWithoutAnimation:tileSet_->selected_index()];
+
+ // Needs to happen after all layers have been added to |rootLayer_|, else
+ // there's a one frame flash of grey at the beginning of the animation
+ // (|bgLayer_| showing through with none of its children visible yet).
+ [[self contentView] setLayer:rootLayer_];
+ [[self contentView] setWantsLayer:YES];
+}
+
+- (BOOL)canBecomeKeyWindow {
+ return YES;
+}
+
+// Handle key events that should be executed repeatedly while the key is down.
+- (void)keyDown:(NSEvent*)event {
+ if (state_ == tabpose::kFadingOut)
+ return;
+ NSString* characters = [event characters];
+ if ([characters length] < 1)
+ return;
+
+ unichar character = [characters characterAtIndex:0];
+ int newIndex = -1;
+ switch (character) {
+ case NSUpArrowFunctionKey:
+ newIndex = tileSet_->up_index();
+ break;
+ case NSDownArrowFunctionKey:
+ newIndex = tileSet_->down_index();
+ break;
+ case NSLeftArrowFunctionKey:
+ newIndex = tileSet_->left_index();
+ break;
+ case NSRightArrowFunctionKey:
+ newIndex = tileSet_->right_index();
+ break;
+ case NSTabCharacter:
+ newIndex = tileSet_->next_index();
+ break;
+ case NSBackTabCharacter:
+ newIndex = tileSet_->previous_index();
+ break;
+ }
+ if (newIndex != -1)
+ [self selectTileAtIndexWithoutAnimation:newIndex];
+}
+
+// Handle keyboard events that should be executed once when the key is released.
+- (void)keyUp:(NSEvent*)event {
+ if (state_ == tabpose::kFadingOut)
+ return;
+ NSString* characters = [event characters];
+ if ([characters length] < 1)
+ return;
+
+ unichar character = [characters characterAtIndex:0];
+ switch (character) {
+ case NSEnterCharacter:
+ case NSNewlineCharacter:
+ case NSCarriageReturnCharacter:
+ case ' ':
+ [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0];
+ break;
+ case '\e': // Escape
+ tileSet_->set_selected_index(tabStripModel_->selected_index());
+ [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0];
+ break;
+ }
+}
+
+// Handle keyboard events that contain cmd or ctrl.
+- (BOOL)performKeyEquivalent:(NSEvent*)event {
+ if (state_ == tabpose::kFadingOut)
+ return NO;
+ NSString* characters = [event characters];
+ if ([characters length] < 1)
+ return NO;
+ unichar character = [characters characterAtIndex:0];
+ if ([event modifierFlags] & NSCommandKeyMask) {
+ if (character >= '1' && character <= '9') {
+ int index =
+ character == '9' ? tabStripModel_->count() - 1 : character - '1';
+ if (index < tabStripModel_->count()) {
+ tileSet_->set_selected_index(index);
+ [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0];
+ return YES;
+ }
+ }
+ }
+ return NO;
+}
+
+-(void)selectTileFromMouseEvent:(NSEvent*)event {
+ int newIndex = -1;
+ CGPoint p = NSPointToCGPoint([event locationInWindow]);
+ for (NSUInteger i = 0; i < [allThumbnailLayers_ count]; ++i) {
+ CALayer* layer = [allThumbnailLayers_ objectAtIndex:i];
+ CGPoint lp = [layer convertPoint:p fromLayer:rootLayer_];
+ if ([static_cast<CALayer*>([layer presentationLayer]) containsPoint:lp])
+ newIndex = i;
+ }
+ if (newIndex >= 0)
+ [self selectTileAtIndexWithoutAnimation:newIndex];
+}
+
+- (void)mouseMoved:(NSEvent*)event {
+ [self selectTileFromMouseEvent:event];
+}
+
+- (void)mouseDown:(NSEvent*)event {
+ // Just in case the user clicked without ever moving the mouse.
+ [self selectTileFromMouseEvent:event];
+
+ [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0];
+}
+
+- (void)swipeWithEvent:(NSEvent*)event {
+ if (abs([event deltaY]) > 0.5) // Swipe up or down.
+ [self fadeAway:([event modifierFlags] & NSShiftKeyMask) != 0];
+}
+
+- (void)close {
+ // Prevent parent window from disappearing.
+ [[self parentWindow] removeChildWindow:self];
+
+ // We're dealloc'd in an autorelease pool – by then the observer registry
+ // might be dead, so explicitly reset the observer now.
+ tabStripModelObserverBridge_.reset();
+
+ [super close];
+}
+
+- (void)commandDispatch:(id)sender {
+ if ([sender tag] == IDC_TABPOSE)
+ [self fadeAway:NO];
+}
+
+- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
+ // Disable all browser-related menu items except the tab overview toggle.
+ SEL action = [item action];
+ NSInteger tag = [item tag];
+ return action == @selector(commandDispatch:) && tag == IDC_TABPOSE;
+}
+
+- (void)fadeAway:(BOOL)slomo {
+ if (state_ == tabpose::kFadingOut)
+ return;
+
+ state_ = tabpose::kFadingOut;
+ [self setAcceptsMouseMovedEvents:NO];
+
+ // Select chosen tab.
+ if (tileSet_->selected_index() < tabStripModel_->count()) {
+ tabStripModel_->SelectTabContentsAt(tileSet_->selected_index(),
+ /*user_gesture=*/true);
+ } else {
+ DCHECK_EQ(tileSet_->selected_index(), 0);
+ }
+
+ {
+ ScopedCAActionDisabler disableCAActions;
+
+ // Move the selected layer on top of all other layers.
+ [self selectedLayer].zPosition = 1;
+
+ selectionHighlight_.hidden = YES;
+ for (CALayer* layer in allFaviconLayers_.get())
+ layer.hidden = YES;
+ for (CALayer* layer in allTitleLayers_.get())
+ layer.hidden = YES;
+
+ // Running animations with shadows is slow, so turn shadows off before
+ // running the exit animation.
+ for (CALayer* layer in allThumbnailLayers_.get())
+ layer.shadowOpacity = 0.0;
+ }
+
+ // Animate layers out, all in one transaction.
+ CGFloat duration =
+ 1.3 * kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1);
+ ScopedCAActionSetDuration durationSetter(duration);
+ for (NSUInteger i = 0; i < [allThumbnailLayers_ count]; ++i) {
+ CALayer* layer = [allThumbnailLayers_ objectAtIndex:i];
+ // |start_thumb_rect_| was relative to the initial index, now this needs to
+ // be relative to |selectedIndex_| (whose start rect was relative to
+ // the initial index, too).
+ CGRect newFrame = NSRectToCGRect(
+ tileSet_->tile_at(i).GetStartRectRelativeTo(tileSet_->selected_tile()));
+
+ // Add a delegate to one of the implicit animations to get a notification
+ // once the animations are done.
+ if (static_cast<int>(i) == tileSet_->selected_index()) {
+ CAAnimation* animation = [CAAnimation animation];
+ animation.delegate = self;
+ [animation setValue:kAnimationIdFadeOut forKey:kAnimationIdKey];
+ [layer addAnimation:animation forKey:@"frame"];
+ }
+
+ layer.frame = newFrame;
+
+ if (static_cast<int>(i) == tileSet_->selected_index()) {
+ // Redraw layer at big resolution, so that zoom-in isn't blocky.
+ [layer setNeedsDisplay];
+ }
+ }
+}
+
+- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
+ NSString* animationId = [animation valueForKey:kAnimationIdKey];
+ if ([animationId isEqualToString:kAnimationIdFadeIn]) {
+ if (finished && state_ == tabpose::kFadingIn) {
+ // If the user clicks while the fade in animation is still running,
+ // |state_| is already kFadingOut. In that case, don't do anything.
+ state_ = tabpose::kFadedIn;
+
+ selectionHighlight_.hidden = NO;
+ for (CALayer* layer in allFaviconLayers_.get())
+ layer.hidden = NO;
+ for (CALayer* layer in allTitleLayers_.get())
+ layer.hidden = NO;
+
+ // Running animations with shadows is slow, so turn shadows on only after
+ // the animation is done.
+ ScopedCAActionDisabler disableCAActions;
+ for (CALayer* layer in allThumbnailLayers_.get())
+ layer.shadowOpacity = 0.5;
+ }
+ } else if ([animationId isEqualToString:kAnimationIdFadeOut]) {
+ DCHECK_EQ(tabpose::kFadingOut, state_);
+ [self close];
+ }
+}
+
+- (NSUInteger)thumbnailLayerCount {
+ return [allThumbnailLayers_ count];
+}
+
+- (int)selectedIndex {
+ return tileSet_->selected_index();
+}
+
+#pragma mark TabStripModelBridge
+
+- (void)refreshLayerFramesAtIndex:(int)i {
+ const tabpose::Tile& tile = tileSet_->tile_at(i);
+
+ CALayer* faviconLayer = [allFaviconLayers_ objectAtIndex:i];
+ faviconLayer.frame = NSRectToCGRect(tile.favicon_rect());
+ CALayer* titleLayer = [allTitleLayers_ objectAtIndex:i];
+ titleLayer.frame = NSRectToCGRect(tile.title_rect());
+ CALayer* thumbLayer = [allThumbnailLayers_ objectAtIndex:i];
+ thumbLayer.frame = NSRectToCGRect(tile.thumb_rect());
+}
+
+- (void)insertTabWithContents:(TabContentsWrapper*)contents
+ atIndex:(NSInteger)index
+ inForeground:(bool)inForeground {
+ // This happens if you cmd-click a link and then immediately open tabpose
+ // on a slowish machine.
+ ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration);
+
+ // Insert new layer and relayout.
+ tileSet_->InsertTileAt(index, contents->tab_contents());
+ tileSet_->Layout(containingRect_);
+ [self addLayersForTile:tileSet_->tile_at(index)
+ showZoom:NO
+ slomo:NO
+ animationDelegate:nil];
+
+ // Update old layers.
+ DCHECK_EQ(tabStripModel_->count(),
+ static_cast<int>([allThumbnailLayers_ count]));
+ DCHECK_EQ(tabStripModel_->count(),
+ static_cast<int>([allTitleLayers_ count]));
+ DCHECK_EQ(tabStripModel_->count(),
+ static_cast<int>([allFaviconLayers_ count]));
+
+ for (int i = 0; i < tabStripModel_->count(); ++i) {
+ if (i == index) // The new layer.
+ continue;
+ [self refreshLayerFramesAtIndex:i];
+ }
+
+ // Update selection.
+ int selectedIndex = tileSet_->selected_index();
+ if (selectedIndex >= index)
+ selectedIndex++;
+ [self selectTileAtIndex:selectedIndex];
+}
+
+- (void)tabClosingWithContents:(TabContentsWrapper*)contents
+ atIndex:(NSInteger)index {
+ // We will also get a -tabDetachedWithContents:atIndex: notification for
+ // closing tabs, so do nothing here.
+}
+
+- (void)tabDetachedWithContents:(TabContentsWrapper*)contents
+ atIndex:(NSInteger)index {
+ ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration);
+
+ // Remove layer and relayout.
+ tileSet_->RemoveTileAt(index);
+ tileSet_->Layout(containingRect_);
+
+ [[allThumbnailLayers_ objectAtIndex:index] removeFromSuperlayer];
+ [allThumbnailLayers_ removeObjectAtIndex:index];
+ [[allTitleLayers_ objectAtIndex:index] removeFromSuperlayer];
+ [allTitleLayers_ removeObjectAtIndex:index];
+ [[allFaviconLayers_ objectAtIndex:index] removeFromSuperlayer];
+ [allFaviconLayers_ removeObjectAtIndex:index];
+
+ // Update old layers.
+ DCHECK_EQ(tabStripModel_->count(),
+ static_cast<int>([allThumbnailLayers_ count]));
+ DCHECK_EQ(tabStripModel_->count(),
+ static_cast<int>([allTitleLayers_ count]));
+ DCHECK_EQ(tabStripModel_->count(),
+ static_cast<int>([allFaviconLayers_ count]));
+
+ if (tabStripModel_->count() == 0)
+ [self close];
+
+ for (int i = 0; i < tabStripModel_->count(); ++i)
+ [self refreshLayerFramesAtIndex:i];
+
+ // Update selection.
+ int selectedIndex = tileSet_->selected_index();
+ if (selectedIndex >= index)
+ selectedIndex--;
+ if (selectedIndex >= 0)
+ [self selectTileAtIndex:selectedIndex];
+}
+
+- (void)tabMovedWithContents:(TabContentsWrapper*)contents
+ fromIndex:(NSInteger)from
+ toIndex:(NSInteger)to {
+ ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration);
+
+ // Move tile from |from| to |to|.
+ tileSet_->MoveTileFromTo(from, to);
+
+ // Move corresponding layers from |from| to |to|.
+ scoped_nsobject<CALayer> thumbLayer(
+ [[allThumbnailLayers_ objectAtIndex:from] retain]);
+ [allThumbnailLayers_ removeObjectAtIndex:from];
+ [allThumbnailLayers_ insertObject:thumbLayer.get() atIndex:to];
+ scoped_nsobject<CALayer> faviconLayer(
+ [[allFaviconLayers_ objectAtIndex:from] retain]);
+ [allFaviconLayers_ removeObjectAtIndex:from];
+ [allFaviconLayers_ insertObject:faviconLayer.get() atIndex:to];
+ scoped_nsobject<CALayer> titleLayer(
+ [[allTitleLayers_ objectAtIndex:from] retain]);
+ [allTitleLayers_ removeObjectAtIndex:from];
+ [allTitleLayers_ insertObject:titleLayer.get() atIndex:to];
+
+ // Update frames of the layers.
+ for (int i = std::min(from, to); i <= std::max(from, to); ++i)
+ [self refreshLayerFramesAtIndex:i];
+
+ // Update selection.
+ int selectedIndex = tileSet_->selected_index();
+ if (from == selectedIndex)
+ selectedIndex = to;
+ else if (from < selectedIndex && selectedIndex <= to)
+ selectedIndex--;
+ else if (to <= selectedIndex && selectedIndex < from)
+ selectedIndex++;
+ [self selectTileAtIndex:selectedIndex];
+}
+
+- (void)tabChangedWithContents:(TabContentsWrapper*)contents
+ atIndex:(NSInteger)index
+ changeType:(TabStripModelObserver::TabChangeType)change {
+ // Tell the window to update text, title, and thumb layers at |index| to get
+ // their data from |contents|. |contents| can be different from the old
+ // contents at that index!
+ // While a tab is loading, this is unfortunately called quite often for
+ // both the "loading" and the "all" change types, so we don't really want to
+ // send thumb requests to the corresponding renderer when this is called.
+ // For now, just make sure that we don't hold on to an invalid TabContents
+ // object.
+ tabpose::Tile& tile = tileSet_->tile_at(index);
+ if (contents->tab_contents() == tile.tab_contents()) {
+ // TODO(thakis): Install a timer to send a thumb request/update title/update
+ // favicon after 20ms or so, and reset the timer every time this is called
+ // to make sure we get an updated thumb, without requesting them all over.
+ return;
+ }
+
+ tile.set_tab_contents(contents->tab_contents());
+ ThumbnailLayer* thumbLayer = [allThumbnailLayers_ objectAtIndex:index];
+ [thumbLayer setTabContents:contents->tab_contents()];
+}
+
+- (void)tabStripModelDeleted {
+ [self close];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/tabpose_window_unittest.mm b/chrome/browser/ui/cocoa/tabpose_window_unittest.mm
new file mode 100644
index 0000000..46f0bca
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tabpose_window_unittest.mm
@@ -0,0 +1,119 @@
+// 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/ui/cocoa/tabpose_window.h"
+
+#import "chrome/browser/browser_window.h"
+#include "chrome/browser/renderer_host/site_instance.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents_wrapper.h"
+#include "chrome/browser/tabs/tab_strip_model.h"
+#import "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+class TabposeWindowTest : public CocoaTest {
+ public:
+ TabposeWindowTest() {
+ site_instance_ =
+ SiteInstance::CreateSiteInstance(browser_helper_.profile());
+ }
+
+ void AppendTabToStrip() {
+ TabContentsWrapper* tab_contents = Browser::TabContentsFactory(
+ browser_helper_.profile(), site_instance_, MSG_ROUTING_NONE,
+ NULL, NULL);
+ browser_helper_.browser()->tabstrip_model()->AppendTabContents(
+ tab_contents, /*foreground=*/true);
+ }
+
+ BrowserTestHelper browser_helper_;
+ scoped_refptr<SiteInstance> site_instance_;
+};
+
+// Check that this doesn't leak.
+TEST_F(TabposeWindowTest, TestShow) {
+ BrowserWindow* browser_window = browser_helper_.CreateBrowserWindow();
+ NSWindow* parent = browser_window->GetNativeHandle();
+
+ [parent orderFront:nil];
+ EXPECT_TRUE([parent isVisible]);
+
+ // Add a few tabs to the tab strip model.
+ for (int i = 0; i < 3; ++i)
+ AppendTabToStrip();
+
+ base::mac::ScopedNSAutoreleasePool pool;
+ TabposeWindow* window =
+ [TabposeWindow openTabposeFor:parent
+ rect:NSMakeRect(10, 20, 250, 160)
+ slomo:NO
+ tabStripModel:browser_helper_.browser()->tabstrip_model()];
+
+ // Should release the window.
+ [window mouseDown:nil];
+
+ browser_helper_.CloseBrowserWindow();
+}
+
+TEST_F(TabposeWindowTest, TestModelObserver) {
+ BrowserWindow* browser_window = browser_helper_.CreateBrowserWindow();
+ NSWindow* parent = browser_window->GetNativeHandle();
+ [parent orderFront:nil];
+
+ // Add a few tabs to the tab strip model.
+ for (int i = 0; i < 3; ++i)
+ AppendTabToStrip();
+
+ base::mac::ScopedNSAutoreleasePool pool;
+ TabposeWindow* window =
+ [TabposeWindow openTabposeFor:parent
+ rect:NSMakeRect(10, 20, 250, 160)
+ slomo:NO
+ tabStripModel:browser_helper_.browser()->tabstrip_model()];
+
+ // Exercise all the model change events.
+ TabStripModel* model = browser_helper_.browser()->tabstrip_model();
+ DCHECK_EQ([window thumbnailLayerCount], 3u);
+ DCHECK_EQ([window selectedIndex], 2);
+
+ model->MoveTabContentsAt(0, 2, /*select_after_move=*/false);
+ DCHECK_EQ([window thumbnailLayerCount], 3u);
+ DCHECK_EQ([window selectedIndex], 1);
+
+ model->MoveTabContentsAt(2, 0, /*select_after_move=*/false);
+ DCHECK_EQ([window thumbnailLayerCount], 3u);
+ DCHECK_EQ([window selectedIndex], 2);
+
+ [window selectTileAtIndexWithoutAnimation:0];
+ DCHECK_EQ([window selectedIndex], 0);
+
+ model->MoveTabContentsAt(0, 2, /*select_after_move=*/false);
+ DCHECK_EQ([window selectedIndex], 2);
+
+ model->MoveTabContentsAt(2, 0, /*select_after_move=*/false);
+ DCHECK_EQ([window selectedIndex], 0);
+
+ delete model->DetachTabContentsAt(0);
+ DCHECK_EQ([window thumbnailLayerCount], 2u);
+ DCHECK_EQ([window selectedIndex], 0);
+
+ AppendTabToStrip();
+ DCHECK_EQ([window thumbnailLayerCount], 3u);
+ DCHECK_EQ([window selectedIndex], 0);
+
+ model->CloseTabContentsAt(0, TabStripModel::CLOSE_NONE);
+ DCHECK_EQ([window thumbnailLayerCount], 2u);
+ DCHECK_EQ([window selectedIndex], 0);
+
+ [window selectTileAtIndexWithoutAnimation:1];
+ model->CloseTabContentsAt(0, TabStripModel::CLOSE_NONE);
+ DCHECK_EQ([window thumbnailLayerCount], 1u);
+ DCHECK_EQ([window selectedIndex], 0);
+
+ // Should release the window.
+ [window mouseDown:nil];
+
+ browser_helper_.CloseBrowserWindow();
+}
diff --git a/chrome/browser/ui/cocoa/task_helpers.h b/chrome/browser/ui/cocoa/task_helpers.h
new file mode 100644
index 0000000..e29c068
--- /dev/null
+++ b/chrome/browser/ui/cocoa/task_helpers.h
@@ -0,0 +1,29 @@
+// 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_UI_COCOA_TASK_HELPERS_H_
+#define CHROME_BROWSER_UI_COCOA_TASK_HELPERS_H_
+#pragma once
+
+class Task;
+
+namespace tracked_objects {
+class Location;
+} // namespace tracked_objects
+
+namespace cocoa_utils {
+
+// This can be used in place of BrowserThread::PostTask(BrowserThread::UI, ...).
+// The purpose of this function is to be able to execute Task work alongside
+// native work when a MessageLoop is blocked by a nested run loop. This function
+// will run the Task in both NSEventTrackingRunLoopMode and NSDefaultRunLoopMode
+// for the purpose of executing work while a menu is open. See
+// http://crbug.com/48679 for the full rationale.
+bool PostTaskInEventTrackingRunLoopMode(
+ const tracked_objects::Location& from_here,
+ Task* task);
+
+} // namespace cocoa_utils
+
+#endif // CHROME_BROWSER_UI_COCOA_TASK_HELPERS_H_
diff --git a/chrome/browser/ui/cocoa/task_helpers.mm b/chrome/browser/ui/cocoa/task_helpers.mm
new file mode 100644
index 0000000..2f8df18
--- /dev/null
+++ b/chrome/browser/ui/cocoa/task_helpers.mm
@@ -0,0 +1,57 @@
+// 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 "chrome/browser/ui/cocoa/task_helpers.h"
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_ptr.h"
+#include "base/task.h"
+
+// This is a wrapper for running Task objects from within a native run loop.
+// This can run specific tasks in that nested loop. This owns the task and will
+// delete it and itself when done.
+@interface NativeTaskRunner : NSObject {
+ @private
+ scoped_ptr<Task> task_;
+}
+- (id)initWithTask:(Task*)task;
+- (void)runTask;
+@end
+
+@implementation NativeTaskRunner
+- (id)initWithTask:(Task*)task {
+ if ((self = [super init])) {
+ task_.reset(task);
+ }
+ return self;
+}
+
+- (void)runTask {
+ task_->Run();
+ [self autorelease];
+}
+@end
+
+namespace cocoa_utils {
+
+bool PostTaskInEventTrackingRunLoopMode(
+ const tracked_objects::Location& from_here,
+ Task* task) {
+ // This deletes itself and the task after the task runs.
+ NativeTaskRunner* runner = [[NativeTaskRunner alloc] initWithTask:task];
+
+ // Schedule the selector in multiple modes in case this was called while a
+ // menu was not running.
+ NSArray* modes = [NSArray arrayWithObjects:NSEventTrackingRunLoopMode,
+ NSDefaultRunLoopMode,
+ nil];
+ [runner performSelectorOnMainThread:@selector(runTask)
+ withObject:nil
+ waitUntilDone:NO
+ modes:modes];
+ return true;
+}
+
+} // namespace cocoa_utils
diff --git a/chrome/browser/ui/cocoa/task_manager_mac.h b/chrome/browser/ui/cocoa/task_manager_mac.h
new file mode 100644
index 0000000..9e6e70b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/task_manager_mac.h
@@ -0,0 +1,118 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_TASK_MANAGER_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_TASK_MANAGER_MAC_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+#include <vector>
+
+#include "base/cocoa_protocols_mac.h"
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/task_manager/task_manager.h"
+#include "chrome/browser/ui/cocoa/table_row_nsimage_cache.h"
+
+@class WindowSizeAutosaver;
+class SkBitmap;
+class TaskManagerMac;
+
+// This class is responsible for loading the task manager window and for
+// managing it.
+@interface TaskManagerWindowController :
+ NSWindowController<NSTableViewDataSource,
+ NSTableViewDelegate> {
+ @private
+ IBOutlet NSTableView* tableView_;
+ IBOutlet NSButton* endProcessButton_;
+ TaskManagerMac* taskManagerObserver_; // weak
+ TaskManager* taskManager_; // weak
+ TaskManagerModel* model_; // weak
+
+ scoped_nsobject<WindowSizeAutosaver> size_saver_;
+
+ // These contain a permutation of [0..|model_->ResourceCount() - 1|]. Used to
+ // implement sorting.
+ std::vector<int> viewToModelMap_;
+ std::vector<int> modelToViewMap_;
+
+ // Descriptor of the current sort column.
+ scoped_nsobject<NSSortDescriptor> currentSortDescriptor_;
+}
+
+// Creates and shows the task manager's window.
+- (id)initWithTaskManagerObserver:(TaskManagerMac*)taskManagerObserver;
+
+// Refreshes all data in the task manager table.
+- (void)reloadData;
+
+// Callback for "Stats for nerds" link.
+- (IBAction)statsLinkClicked:(id)sender;
+
+// Callback for "End process" button.
+- (IBAction)killSelectedProcesses:(id)sender;
+
+// Callback for double clicks on the table.
+- (void)selectDoubleClickedTab:(id)sender;
+@end
+
+@interface TaskManagerWindowController (TestingAPI)
+- (NSTableView*)tableView;
+@end
+
+// This class listens to task changed events sent by chrome.
+class TaskManagerMac : public TaskManagerModelObserver,
+ public TableRowNSImageCache::Table {
+ public:
+ TaskManagerMac(TaskManager* task_manager);
+ virtual ~TaskManagerMac();
+
+ // TaskManagerModelObserver
+ virtual void OnModelChanged();
+ virtual void OnItemsChanged(int start, int length);
+ virtual void OnItemsAdded(int start, int length);
+ virtual void OnItemsRemoved(int start, int length);
+
+ // Called by the cocoa window controller when its window closes and the
+ // controller destroyed itself. Informs the model to stop updating.
+ void WindowWasClosed();
+
+ // TableRowNSImageCache::Table
+ virtual int RowCount() const;
+ virtual SkBitmap GetIcon(int r) const;
+
+ // Creates the task manager if it doesn't exist; otherwise, it activates the
+ // existing task manager window.
+ static void Show();
+
+ // Returns the TaskManager observed by |this|.
+ TaskManager* task_manager() { return task_manager_; }
+
+ // Lazily converts the image at the given row and caches it in |icon_cache_|.
+ NSImage* GetImageForRow(int row);
+
+ // Returns the cocoa object. Used for testing.
+ TaskManagerWindowController* cocoa_controller() { return window_controller_; }
+ private:
+ // The task manager.
+ TaskManager* const task_manager_; // weak
+
+ // Our model.
+ TaskManagerModel* const model_; // weak
+
+ // Controller of our window, destroys itself when the task manager window
+ // is closed.
+ TaskManagerWindowController* window_controller_; // weak
+
+ // Caches favicons for all rows. Needs to be initalized after |model_|.
+ TableRowNSImageCache icon_cache_;
+
+ // An open task manager window. There can only be one open at a time. This
+ // is reset to NULL when the window is closed.
+ static TaskManagerMac* instance_;
+
+ DISALLOW_COPY_AND_ASSIGN(TaskManagerMac);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_TASK_MANAGER_MAC_H_
diff --git a/chrome/browser/ui/cocoa/task_manager_mac.mm b/chrome/browser/ui/cocoa/task_manager_mac.mm
new file mode 100644
index 0000000..c11564c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/task_manager_mac.mm
@@ -0,0 +1,582 @@
+// 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 "chrome/browser/ui/cocoa/task_manager_mac.h"
+
+#include <algorithm>
+#include <vector>
+
+#include "app/l10n_util_mac.h"
+#include "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/browser_process.h"
+#import "chrome/browser/ui/cocoa/window_size_autosaver.h"
+#include "chrome/common/pref_names.h"
+#include "grit/generated_resources.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+namespace {
+
+// Width of "a" and most other letters/digits in "small" table views.
+const int kCharWidth = 6;
+
+// Some of the strings below have spaces at the end or are missing letters, to
+// make the columns look nicer, and to take potentially longer localized strings
+// into account.
+const struct ColumnWidth {
+ int columnId;
+ int minWidth;
+ int maxWidth; // If this is -1, 1.5*minColumWidth is used as max width.
+} columnWidths[] = {
+ // Note that arraysize includes the trailing \0. That's intended.
+ { IDS_TASK_MANAGER_PAGE_COLUMN, 120, 600 },
+ { IDS_TASK_MANAGER_PHYSICAL_MEM_COLUMN,
+ arraysize("800 MiB") * kCharWidth, -1 },
+ { IDS_TASK_MANAGER_SHARED_MEM_COLUMN,
+ arraysize("800 MiB") * kCharWidth, -1 },
+ { IDS_TASK_MANAGER_PRIVATE_MEM_COLUMN,
+ arraysize("800 MiB") * kCharWidth, -1 },
+ { IDS_TASK_MANAGER_CPU_COLUMN,
+ arraysize("99.9") * kCharWidth, -1 },
+ { IDS_TASK_MANAGER_NET_COLUMN,
+ arraysize("150 kiB/s") * kCharWidth, -1 },
+ { IDS_TASK_MANAGER_PROCESS_ID_COLUMN,
+ arraysize("73099 ") * kCharWidth, -1 },
+ { IDS_TASK_MANAGER_WEBCORE_IMAGE_CACHE_COLUMN,
+ arraysize("2000.0K (2000.0 live)") * kCharWidth, -1 },
+ { IDS_TASK_MANAGER_WEBCORE_SCRIPTS_CACHE_COLUMN,
+ arraysize("2000.0K (2000.0 live)") * kCharWidth, -1 },
+ { IDS_TASK_MANAGER_WEBCORE_CSS_CACHE_COLUMN,
+ arraysize("2000.0K (2000.0 live)") * kCharWidth, -1 },
+ { IDS_TASK_MANAGER_SQLITE_MEMORY_USED_COLUMN,
+ arraysize("800 kB") * kCharWidth, -1 },
+ { IDS_TASK_MANAGER_JAVASCRIPT_MEMORY_ALLOCATED_COLUMN,
+ arraysize("2000.0K (2000.0 live)") * kCharWidth, -1 },
+ { IDS_TASK_MANAGER_GOATS_TELEPORTED_COLUMN,
+ arraysize("15 ") * kCharWidth, -1 },
+};
+
+class SortHelper {
+ public:
+ SortHelper(TaskManagerModel* model, NSSortDescriptor* column)
+ : sort_column_([[column key] intValue]),
+ ascending_([column ascending]),
+ model_(model) {}
+
+ bool operator()(int a, int b) {
+ std::pair<int, int> group_range1 = model_->GetGroupRangeForResource(a);
+ std::pair<int, int> group_range2 = model_->GetGroupRangeForResource(b);
+ if (group_range1 == group_range2) {
+ // The two rows are in the same group, sort so that items in the same
+ // group always appear in the same order. |ascending_| is intentionally
+ // ignored.
+ return a < b;
+ }
+ // Sort by the first entry of each of the groups.
+ int cmp_result = model_->CompareValues(
+ group_range1.first, group_range2.first, sort_column_);
+ if (!ascending_)
+ cmp_result = -cmp_result;
+ return cmp_result < 0;
+ }
+ private:
+ int sort_column_;
+ bool ascending_;
+ TaskManagerModel* model_; // weak;
+};
+
+} // namespace
+
+@interface TaskManagerWindowController (Private)
+- (NSTableColumn*)addColumnWithId:(int)columnId visible:(BOOL)isVisible;
+- (void)setUpTableColumns;
+- (void)setUpTableHeaderContextMenu;
+- (void)toggleColumn:(id)sender;
+- (void)adjustSelectionAndEndProcessButton;
+- (void)deselectRows;
+@end
+
+////////////////////////////////////////////////////////////////////////////////
+// TaskManagerWindowController implementation:
+
+@implementation TaskManagerWindowController
+
+- (id)initWithTaskManagerObserver:(TaskManagerMac*)taskManagerObserver {
+ NSString* nibpath = [mac_util::MainAppBundle()
+ pathForResource:@"TaskManager"
+ ofType:@"nib"];
+ if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
+ taskManagerObserver_ = taskManagerObserver;
+ taskManager_ = taskManagerObserver_->task_manager();
+ model_ = taskManager_->model();
+
+ if (g_browser_process && g_browser_process->local_state()) {
+ size_saver_.reset([[WindowSizeAutosaver alloc]
+ initWithWindow:[self window]
+ prefService:g_browser_process->local_state()
+ path:prefs::kTaskManagerWindowPlacement]);
+ }
+ [self showWindow:self];
+ }
+ return self;
+}
+
+- (void)sortShuffleArray {
+ viewToModelMap_.resize(model_->ResourceCount());
+ for (size_t i = 0; i < viewToModelMap_.size(); ++i)
+ viewToModelMap_[i] = i;
+
+ std::sort(viewToModelMap_.begin(), viewToModelMap_.end(),
+ SortHelper(model_, currentSortDescriptor_.get()));
+
+ modelToViewMap_.resize(viewToModelMap_.size());
+ for (size_t i = 0; i < viewToModelMap_.size(); ++i)
+ modelToViewMap_[viewToModelMap_[i]] = i;
+}
+
+- (void)reloadData {
+ // Store old view indices, and the model indices they map to.
+ NSIndexSet* viewSelection = [tableView_ selectedRowIndexes];
+ std::vector<int> modelSelection;
+ for (NSUInteger i = [viewSelection lastIndex];
+ i != NSNotFound;
+ i = [viewSelection indexLessThanIndex:i]) {
+ modelSelection.push_back(viewToModelMap_[i]);
+ }
+
+ // Sort.
+ [self sortShuffleArray];
+
+ // Use the model indices to get the new view indices of the selection, and
+ // set selection to that. This assumes that no rows were added or removed
+ // (in that case, the selection is cleared before -reloadData is called).
+ if (modelSelection.size() > 0)
+ DCHECK_EQ([tableView_ numberOfRows], model_->ResourceCount());
+ NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
+ for (size_t i = 0; i < modelSelection.size(); ++i)
+ [indexSet addIndex:modelToViewMap_[modelSelection[i]]];
+ [tableView_ selectRowIndexes:indexSet byExtendingSelection:NO];
+
+ [tableView_ reloadData];
+ [self adjustSelectionAndEndProcessButton];
+}
+
+- (IBAction)statsLinkClicked:(id)sender {
+ TaskManager::GetInstance()->OpenAboutMemory();
+}
+
+- (IBAction)killSelectedProcesses:(id)sender {
+ NSIndexSet* selection = [tableView_ selectedRowIndexes];
+ for (NSUInteger i = [selection lastIndex];
+ i != NSNotFound;
+ i = [selection indexLessThanIndex:i]) {
+ taskManager_->KillProcess(viewToModelMap_[i]);
+ }
+}
+
+- (void)selectDoubleClickedTab:(id)sender {
+ NSInteger row = [tableView_ clickedRow];
+ if (row < 0)
+ return; // Happens e.g. if the table header is double-clicked.
+ taskManager_->ActivateProcess(viewToModelMap_[row]);
+}
+
+- (NSTableView*)tableView {
+ return tableView_;
+}
+
+- (void)awakeFromNib {
+ [self setUpTableColumns];
+ [self setUpTableHeaderContextMenu];
+ [self adjustSelectionAndEndProcessButton];
+
+ [tableView_ setDoubleAction:@selector(selectDoubleClickedTab:)];
+ [tableView_ sizeToFit];
+}
+
+- (void)dealloc {
+ [tableView_ setDelegate:nil];
+ [tableView_ setDataSource:nil];
+ [super dealloc];
+}
+
+// Adds a column which has the given string id as title. |isVisible| specifies
+// if the column is initially visible.
+- (NSTableColumn*)addColumnWithId:(int)columnId visible:(BOOL)isVisible {
+ scoped_nsobject<NSTableColumn> column([[NSTableColumn alloc]
+ initWithIdentifier:[NSNumber numberWithInt:columnId]]);
+
+ NSTextAlignment textAlignment = columnId == IDS_TASK_MANAGER_PAGE_COLUMN ?
+ NSLeftTextAlignment : NSRightTextAlignment;
+
+ [[column.get() headerCell]
+ setStringValue:l10n_util::GetNSStringWithFixup(columnId)];
+ [[column.get() headerCell] setAlignment:textAlignment];
+ [[column.get() dataCell] setAlignment:textAlignment];
+
+ NSFont* font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
+ [[column.get() dataCell] setFont:font];
+
+ [column.get() setHidden:!isVisible];
+ [column.get() setEditable:NO];
+
+ // The page column should by default be sorted ascending.
+ BOOL ascending = columnId == IDS_TASK_MANAGER_PAGE_COLUMN;
+
+ scoped_nsobject<NSSortDescriptor> sortDescriptor([[NSSortDescriptor alloc]
+ initWithKey:[NSString stringWithFormat:@"%d", columnId]
+ ascending:ascending]);
+ [column.get() setSortDescriptorPrototype:sortDescriptor.get()];
+
+ // Default values, only used in release builds if nobody notices the DCHECK
+ // during development when adding new columns.
+ int minWidth = 200, maxWidth = 400;
+
+ size_t i;
+ for (i = 0; i < arraysize(columnWidths); ++i) {
+ if (columnWidths[i].columnId == columnId) {
+ minWidth = columnWidths[i].minWidth;
+ maxWidth = columnWidths[i].maxWidth;
+ if (maxWidth < 0)
+ maxWidth = 3 * minWidth / 2; // *1.5 for ints.
+ break;
+ }
+ }
+ DCHECK(i < arraysize(columnWidths)) << "Could not find " << columnId;
+ [column.get() setMinWidth:minWidth];
+ [column.get() setMaxWidth:maxWidth];
+ [column.get() setResizingMask:NSTableColumnAutoresizingMask |
+ NSTableColumnUserResizingMask];
+
+ [tableView_ addTableColumn:column.get()];
+ return column.get(); // Now retained by |tableView_|.
+}
+
+// Adds all the task manager's columns to the table.
+- (void)setUpTableColumns {
+ for (NSTableColumn* column in [tableView_ tableColumns])
+ [tableView_ removeTableColumn:column];
+ NSTableColumn* nameColumn = [self addColumnWithId:IDS_TASK_MANAGER_PAGE_COLUMN
+ visible:YES];
+ // |nameColumn| displays an icon for every row -- this is done by an
+ // NSButtonCell.
+ scoped_nsobject<NSButtonCell> nameCell(
+ [[NSButtonCell alloc] initTextCell:@""]);
+ [nameCell.get() setImagePosition:NSImageLeft];
+ [nameCell.get() setButtonType:NSSwitchButton];
+ [nameCell.get() setAlignment:[[nameColumn dataCell] alignment]];
+ [nameCell.get() setFont:[[nameColumn dataCell] font]];
+ [nameColumn setDataCell:nameCell.get()];
+
+ // Initially, sort on the tab name.
+ [tableView_ setSortDescriptors:
+ [NSArray arrayWithObject:[nameColumn sortDescriptorPrototype]]];
+
+ [self addColumnWithId:IDS_TASK_MANAGER_PHYSICAL_MEM_COLUMN visible:YES];
+ [self addColumnWithId:IDS_TASK_MANAGER_SHARED_MEM_COLUMN visible:NO];
+ [self addColumnWithId:IDS_TASK_MANAGER_PRIVATE_MEM_COLUMN visible:NO];
+ [self addColumnWithId:IDS_TASK_MANAGER_CPU_COLUMN visible:YES];
+ [self addColumnWithId:IDS_TASK_MANAGER_NET_COLUMN visible:YES];
+ [self addColumnWithId:IDS_TASK_MANAGER_PROCESS_ID_COLUMN visible:NO];
+ [self addColumnWithId:IDS_TASK_MANAGER_WEBCORE_IMAGE_CACHE_COLUMN
+ visible:NO];
+ [self addColumnWithId:IDS_TASK_MANAGER_WEBCORE_SCRIPTS_CACHE_COLUMN
+ visible:NO];
+ [self addColumnWithId:IDS_TASK_MANAGER_WEBCORE_CSS_CACHE_COLUMN visible:NO];
+ [self addColumnWithId:IDS_TASK_MANAGER_SQLITE_MEMORY_USED_COLUMN visible:NO];
+ [self addColumnWithId:IDS_TASK_MANAGER_JAVASCRIPT_MEMORY_ALLOCATED_COLUMN
+ visible:NO];
+ [self addColumnWithId:IDS_TASK_MANAGER_GOATS_TELEPORTED_COLUMN visible:NO];
+}
+
+// Creates a context menu for the table header that allows the user to toggle
+// which columns should be shown and which should be hidden (like e.g.
+// Task Manager.app's table header context menu).
+- (void)setUpTableHeaderContextMenu {
+ scoped_nsobject<NSMenu> contextMenu(
+ [[NSMenu alloc] initWithTitle:@"Task Manager context menu"]);
+ for (NSTableColumn* column in [tableView_ tableColumns]) {
+ NSMenuItem* item = [contextMenu.get()
+ addItemWithTitle:[[column headerCell] stringValue]
+ action:@selector(toggleColumn:)
+ keyEquivalent:@""];
+ [item setTarget:self];
+ [item setRepresentedObject:column];
+ [item setState:[column isHidden] ? NSOffState : NSOnState];
+ }
+ [[tableView_ headerView] setMenu:contextMenu.get()];
+}
+
+// Callback for the table header context menu. Toggles visibility of the table
+// column associated with the clicked menu item.
+- (void)toggleColumn:(id)item {
+ DCHECK([item isKindOfClass:[NSMenuItem class]]);
+ if (![item isKindOfClass:[NSMenuItem class]])
+ return;
+
+ NSTableColumn* column = [item representedObject];
+ DCHECK(column);
+ NSInteger oldState = [item state];
+ NSInteger newState = oldState == NSOnState ? NSOffState : NSOnState;
+ [column setHidden:newState == NSOffState];
+ [item setState:newState];
+ [tableView_ sizeToFit];
+ [tableView_ setNeedsDisplay];
+}
+
+// This function appropriately sets the enabled states on the table's editing
+// buttons.
+- (void)adjustSelectionAndEndProcessButton {
+ bool selectionContainsBrowserProcess = false;
+
+ // If a row is selected, make sure that all rows belonging to the same process
+ // are selected as well. Also, check if the selection contains the browser
+ // process.
+ NSIndexSet* selection = [tableView_ selectedRowIndexes];
+ for (NSUInteger i = [selection lastIndex];
+ i != NSNotFound;
+ i = [selection indexLessThanIndex:i]) {
+ int modelIndex = viewToModelMap_[i];
+ if (taskManager_->IsBrowserProcess(modelIndex))
+ selectionContainsBrowserProcess = true;
+
+ std::pair<int, int> rangePair =
+ model_->GetGroupRangeForResource(modelIndex);
+ NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
+ for (int j = 0; j < rangePair.second; ++j)
+ [indexSet addIndex:modelToViewMap_[rangePair.first + j]];
+ [tableView_ selectRowIndexes:indexSet byExtendingSelection:YES];
+ }
+
+ bool enabled = [selection count] > 0 && !selectionContainsBrowserProcess;
+ [endProcessButton_ setEnabled:enabled];
+}
+
+- (void)deselectRows {
+ [tableView_ deselectAll:self];
+}
+
+// Table view delegate method.
+- (void)tableViewSelectionIsChanging:(NSNotification*)aNotification {
+ [self adjustSelectionAndEndProcessButton];
+}
+
+- (void)windowWillClose:(NSNotification*)notification {
+ if (taskManagerObserver_) {
+ taskManagerObserver_->WindowWasClosed();
+ taskManagerObserver_ = nil;
+ }
+ [self autorelease];
+}
+
+@end
+
+@implementation TaskManagerWindowController (NSTableDataSource)
+
+- (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView {
+ DCHECK(tableView == tableView_ || tableView_ == nil);
+ return model_->ResourceCount();
+}
+
+- (NSString*)modelTextForRow:(int)row column:(int)columnId {
+ DCHECK_LT(static_cast<size_t>(row), viewToModelMap_.size());
+ row = viewToModelMap_[row];
+ switch (columnId) {
+ case IDS_TASK_MANAGER_PAGE_COLUMN: // Process
+ return base::SysUTF16ToNSString(model_->GetResourceTitle(row));
+
+ case IDS_TASK_MANAGER_PRIVATE_MEM_COLUMN: // Memory
+ if (!model_->IsResourceFirstInGroup(row))
+ return @"";
+ return base::SysUTF16ToNSString(model_->GetResourcePrivateMemory(row));
+
+ case IDS_TASK_MANAGER_SHARED_MEM_COLUMN: // Memory
+ if (!model_->IsResourceFirstInGroup(row))
+ return @"";
+ return base::SysUTF16ToNSString(model_->GetResourceSharedMemory(row));
+
+ case IDS_TASK_MANAGER_PHYSICAL_MEM_COLUMN: // Memory
+ if (!model_->IsResourceFirstInGroup(row))
+ return @"";
+ return base::SysUTF16ToNSString(model_->GetResourcePhysicalMemory(row));
+
+ case IDS_TASK_MANAGER_CPU_COLUMN: // CPU
+ if (!model_->IsResourceFirstInGroup(row))
+ return @"";
+ return base::SysUTF16ToNSString(model_->GetResourceCPUUsage(row));
+
+ case IDS_TASK_MANAGER_NET_COLUMN: // Net
+ return base::SysUTF16ToNSString(model_->GetResourceNetworkUsage(row));
+
+ case IDS_TASK_MANAGER_PROCESS_ID_COLUMN: // Process ID
+ if (!model_->IsResourceFirstInGroup(row))
+ return @"";
+ return base::SysUTF16ToNSString(model_->GetResourceProcessId(row));
+
+ case IDS_TASK_MANAGER_WEBCORE_IMAGE_CACHE_COLUMN: // WebCore image cache
+ if (!model_->IsResourceFirstInGroup(row))
+ return @"";
+ return base::SysUTF16ToNSString(
+ model_->GetResourceWebCoreImageCacheSize(row));
+
+ case IDS_TASK_MANAGER_WEBCORE_SCRIPTS_CACHE_COLUMN: // WebCore script cache
+ if (!model_->IsResourceFirstInGroup(row))
+ return @"";
+ return base::SysUTF16ToNSString(
+ model_->GetResourceWebCoreScriptsCacheSize(row));
+
+ case IDS_TASK_MANAGER_WEBCORE_CSS_CACHE_COLUMN: // WebCore CSS cache
+ if (!model_->IsResourceFirstInGroup(row))
+ return @"";
+ return base::SysUTF16ToNSString(
+ model_->GetResourceWebCoreCSSCacheSize(row));
+
+ case IDS_TASK_MANAGER_SQLITE_MEMORY_USED_COLUMN:
+ if (!model_->IsResourceFirstInGroup(row))
+ return @"";
+ return base::SysUTF16ToNSString(
+ model_->GetResourceSqliteMemoryUsed(row));
+
+ case IDS_TASK_MANAGER_JAVASCRIPT_MEMORY_ALLOCATED_COLUMN:
+ if (!model_->IsResourceFirstInGroup(row))
+ return @"";
+ return base::SysUTF16ToNSString(
+ model_->GetResourceV8MemoryAllocatedSize(row));
+
+ case IDS_TASK_MANAGER_GOATS_TELEPORTED_COLUMN: // Goats Teleported!
+ return base::SysUTF16ToNSString(model_->GetResourceGoatsTeleported(row));
+
+ default:
+ NOTREACHED();
+ return @"";
+ }
+}
+
+- (id)tableView:(NSTableView*)tableView
+ objectValueForTableColumn:(NSTableColumn*)tableColumn
+ row:(NSInteger)rowIndex {
+ // NSButtonCells expect an on/off state as objectValue. Their title is set
+ // in |tableView:dataCellForTableColumn:row:| below.
+ if ([[tableColumn identifier] intValue] == IDS_TASK_MANAGER_PAGE_COLUMN) {
+ return [NSNumber numberWithInt:NSOffState];
+ }
+
+ return [self modelTextForRow:rowIndex
+ column:[[tableColumn identifier] intValue]];
+}
+
+- (NSCell*)tableView:(NSTableView*)tableView
+ dataCellForTableColumn:(NSTableColumn*)tableColumn
+ row:(NSInteger)rowIndex {
+ NSCell* cell = [tableColumn dataCellForRow:rowIndex];
+
+ // Set the favicon and title for the task in the name column.
+ if ([[tableColumn identifier] intValue] == IDS_TASK_MANAGER_PAGE_COLUMN) {
+ DCHECK([cell isKindOfClass:[NSButtonCell class]]);
+ NSButtonCell* buttonCell = static_cast<NSButtonCell*>(cell);
+ NSString* title = [self modelTextForRow:rowIndex
+ column:[[tableColumn identifier] intValue]];
+ [buttonCell setTitle:title];
+ [buttonCell setImage:
+ taskManagerObserver_->GetImageForRow(viewToModelMap_[rowIndex])];
+ [buttonCell setRefusesFirstResponder:YES]; // Don't push in like a button.
+ [buttonCell setHighlightsBy:NSNoCellMask];
+ }
+
+ return cell;
+}
+
+- (void) tableView:(NSTableView*)tableView
+ sortDescriptorsDidChange:(NSArray*)oldDescriptors {
+ NSArray* newDescriptors = [tableView sortDescriptors];
+ if ([newDescriptors count] < 1)
+ return;
+
+ currentSortDescriptor_.reset([[newDescriptors objectAtIndex:0] retain]);
+ [self reloadData]; // Sorts.
+}
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////
+// TaskManagerMac implementation:
+
+TaskManagerMac::TaskManagerMac(TaskManager* task_manager)
+ : task_manager_(task_manager),
+ model_(task_manager->model()),
+ icon_cache_(this) {
+ window_controller_ =
+ [[TaskManagerWindowController alloc] initWithTaskManagerObserver:this];
+ model_->AddObserver(this);
+}
+
+// static
+TaskManagerMac* TaskManagerMac::instance_ = NULL;
+
+TaskManagerMac::~TaskManagerMac() {
+ if (this == instance_) {
+ // Do not do this when running in unit tests: |StartUpdating()| never got
+ // called in that case.
+ task_manager_->OnWindowClosed();
+ }
+ model_->RemoveObserver(this);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// TaskManagerMac, TaskManagerModelObserver implementation:
+
+void TaskManagerMac::OnModelChanged() {
+ icon_cache_.OnModelChanged();
+ [window_controller_ deselectRows];
+ [window_controller_ reloadData];
+}
+
+void TaskManagerMac::OnItemsChanged(int start, int length) {
+ icon_cache_.OnItemsChanged(start, length);
+ [window_controller_ reloadData];
+}
+
+void TaskManagerMac::OnItemsAdded(int start, int length) {
+ icon_cache_.OnItemsAdded(start, length);
+ [window_controller_ deselectRows];
+ [window_controller_ reloadData];
+}
+
+void TaskManagerMac::OnItemsRemoved(int start, int length) {
+ icon_cache_.OnItemsRemoved(start, length);
+ [window_controller_ deselectRows];
+ [window_controller_ reloadData];
+}
+
+NSImage* TaskManagerMac::GetImageForRow(int row) {
+ return icon_cache_.GetImageForRow(row);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// TaskManagerMac, public:
+
+void TaskManagerMac::WindowWasClosed() {
+ delete this;
+ instance_ = NULL;
+}
+
+int TaskManagerMac::RowCount() const {
+ return model_->ResourceCount();
+}
+
+SkBitmap TaskManagerMac::GetIcon(int r) const {
+ return model_->GetResourceIcon(r);
+}
+
+// static
+void TaskManagerMac::Show() {
+ if (instance_) {
+ // If there's a Task manager window open already, just activate it.
+ [[instance_->window_controller_ window]
+ makeKeyAndOrderFront:instance_->window_controller_];
+ } else {
+ instance_ = new TaskManagerMac(TaskManager::GetInstance());
+ instance_->model_->StartUpdating();
+ }
+}
diff --git a/chrome/browser/ui/cocoa/task_manager_mac_unittest.mm b/chrome/browser/ui/cocoa/task_manager_mac_unittest.mm
new file mode 100644
index 0000000..2a7aae3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/task_manager_mac_unittest.mm
@@ -0,0 +1,115 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "base/utf_string_conversions.h"
+#import "chrome/browser/ui/cocoa/task_manager_mac.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "grit/generated_resources.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+namespace {
+
+class TestResource : public TaskManager::Resource {
+ public:
+ TestResource(const string16& title, pid_t pid) : title_(title), pid_(pid) {}
+ virtual std::wstring GetTitle() const { return UTF16ToWide(title_); }
+ virtual SkBitmap GetIcon() const { return SkBitmap(); }
+ virtual base::ProcessHandle GetProcess() const { return pid_; }
+ virtual Type GetType() const { return RENDERER; }
+ virtual bool SupportNetworkUsage() const { return false; }
+ virtual void SetSupportNetworkUsage() { NOTREACHED(); }
+ virtual void Refresh() {}
+ string16 title_;
+ pid_t pid_;
+};
+
+} // namespace
+
+class TaskManagerWindowControllerTest : public CocoaTest {
+};
+
+// Test creation, to ensure nothing leaks or crashes.
+TEST_F(TaskManagerWindowControllerTest, Init) {
+ TaskManager task_manager;
+ TaskManagerMac* bridge(new TaskManagerMac(&task_manager));
+ TaskManagerWindowController* controller = bridge->cocoa_controller();
+
+ // Releases the controller, which in turn deletes |bridge|.
+ [controller close];
+}
+
+TEST_F(TaskManagerWindowControllerTest, Sort) {
+ TaskManager task_manager;
+
+ TestResource resource1(UTF8ToUTF16("zzz"), 1);
+ TestResource resource2(UTF8ToUTF16("zzb"), 2);
+ TestResource resource3(UTF8ToUTF16("zza"), 2);
+
+ task_manager.AddResource(&resource1);
+ task_manager.AddResource(&resource2);
+ task_manager.AddResource(&resource3); // Will be in the same group as 2.
+
+ TaskManagerMac* bridge(new TaskManagerMac(&task_manager));
+ TaskManagerWindowController* controller = bridge->cocoa_controller();
+ NSTableView* table = [controller tableView];
+ ASSERT_EQ(3, [controller numberOfRowsInTableView:table]);
+
+ // Test that table is sorted on title.
+ NSTableColumn* title_column = [table tableColumnWithIdentifier:
+ [NSNumber numberWithInt:IDS_TASK_MANAGER_PAGE_COLUMN]];
+ NSCell* cell;
+ cell = [controller tableView:table dataCellForTableColumn:title_column row:0];
+ EXPECT_NSEQ(@"zzb", [cell title]);
+ cell = [controller tableView:table dataCellForTableColumn:title_column row:1];
+ EXPECT_NSEQ(@"zza", [cell title]);
+ cell = [controller tableView:table dataCellForTableColumn:title_column row:2];
+ EXPECT_NSEQ(@"zzz", [cell title]);
+
+ // Releases the controller, which in turn deletes |bridge|.
+ [controller close];
+
+ task_manager.RemoveResource(&resource1);
+ task_manager.RemoveResource(&resource2);
+ task_manager.RemoveResource(&resource3);
+}
+
+TEST_F(TaskManagerWindowControllerTest, SelectionAdaptsToSorting) {
+ TaskManager task_manager;
+
+ TestResource resource1(UTF8ToUTF16("yyy"), 1);
+ TestResource resource2(UTF8ToUTF16("aaa"), 2);
+
+ task_manager.AddResource(&resource1);
+ task_manager.AddResource(&resource2);
+
+ TaskManagerMac* bridge(new TaskManagerMac(&task_manager));
+ TaskManagerWindowController* controller = bridge->cocoa_controller();
+ NSTableView* table = [controller tableView];
+ ASSERT_EQ(2, [controller numberOfRowsInTableView:table]);
+
+ // Select row 0 in the table (corresponds to row 1 in the model).
+ [table selectRowIndexes:[NSIndexSet indexSetWithIndex:0]
+ byExtendingSelection:NO];
+
+ // Change the name of resource2 so that it becomes row 1 in the table.
+ resource2.title_ = UTF8ToUTF16("zzz");
+ bridge->OnItemsChanged(1, 1);
+
+ // Check that the selection has moved to row 1.
+ NSIndexSet* selection = [table selectedRowIndexes];
+ ASSERT_EQ(1u, [selection count]);
+ EXPECT_EQ(1u, [selection firstIndex]);
+
+ // Releases the controller, which in turn deletes |bridge|.
+ [controller close];
+
+ task_manager.RemoveResource(&resource1);
+ task_manager.RemoveResource(&resource2);
+}
diff --git a/chrome/browser/ui/cocoa/test_event_utils.h b/chrome/browser/ui/cocoa/test_event_utils.h
new file mode 100644
index 0000000..43bc78f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/test_event_utils.h
@@ -0,0 +1,48 @@
+// 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_UI_COCOA_TEST_EVENT_UTILS_H_
+#define CHROME_BROWSER_UI_COCOA_TEST_EVENT_UTILS_H_
+#pragma once
+
+#include <utility>
+
+#import <objc/objc-class.h>
+
+#include "base/basictypes.h"
+
+// Within a given scope, replace the selector |selector| on |target| with that
+// from |source|.
+class ScopedClassSwizzler {
+ public:
+ ScopedClassSwizzler(Class target, Class source, SEL selector);
+ ~ScopedClassSwizzler();
+
+ private:
+ Method old_selector_impl_;
+ Method new_selector_impl_;
+
+ DISALLOW_COPY_AND_ASSIGN(ScopedClassSwizzler);
+};
+
+namespace test_event_utils {
+
+// 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);
+
+// Return a mouse down and an up event with the given |clickCount| at
+// |view|'s midpoint.
+std::pair<NSEvent*, NSEvent*> MouseClickInView(NSView* view,
+ NSUInteger clickCount);
+
+} // namespace test_event_utils
+
+#endif // CHROME_BROWSER_UI_COCOA_TEST_EVENT_UTILS_H_
diff --git a/chrome/browser/ui/cocoa/test_event_utils.mm b/chrome/browser/ui/cocoa/test_event_utils.mm
new file mode 100644
index 0000000..9675db6
--- /dev/null
+++ b/chrome/browser/ui/cocoa/test_event_utils.mm
@@ -0,0 +1,86 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/ui/cocoa/test_event_utils.h"
+
+ScopedClassSwizzler::ScopedClassSwizzler(Class target, Class source,
+ SEL selector) {
+ old_selector_impl_ = class_getInstanceMethod(target, selector);
+ new_selector_impl_ = class_getInstanceMethod(source, selector);
+ method_exchangeImplementations(old_selector_impl_, new_selector_impl_);
+}
+
+ScopedClassSwizzler::~ScopedClassSwizzler() {
+ method_exchangeImplementations(old_selector_impl_, new_selector_impl_);
+}
+
+namespace test_event_utils {
+
+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 = { point.x, point.y };
+ CGEventRef cg_event = CGEventCreateMouseEvent(NULL, kCGEventOtherMouseUp,
+ location,
+ kCGMouseButtonCenter);
+ NSEvent* event = [NSEvent eventWithCGEvent:cg_event];
+ CFRelease(cg_event);
+ return event;
+ }
+ return [NSEvent mouseEventWithType:type
+ location:point
+ modifierFlags:modifiers
+ timestamp:0
+ windowNumber:0
+ context:nil
+ eventNumber:0
+ clickCount:1
+ pressure:1.0];
+}
+
+NSEvent* MakeMouseEvent(NSEventType type, NSUInteger modifiers) {
+ return MouseEventAtPoint(NSMakePoint(0, 0), type, modifiers);
+}
+
+static NSEvent* MouseEventAtPointInWindow(NSPoint point,
+ NSEventType type,
+ NSWindow* window,
+ NSUInteger clickCount) {
+ return [NSEvent mouseEventWithType:type
+ location:point
+ modifierFlags:0
+ timestamp:0
+ windowNumber:[window windowNumber]
+ context:nil
+ eventNumber:0
+ clickCount:clickCount
+ pressure:1.0];
+}
+
+NSEvent* LeftMouseDownAtPointInWindow(NSPoint point, NSWindow* window) {
+ return MouseEventAtPointInWindow(point, NSLeftMouseDown, window, 1);
+}
+
+NSEvent* LeftMouseDownAtPoint(NSPoint point) {
+ return LeftMouseDownAtPointInWindow(point, nil);
+}
+
+std::pair<NSEvent*,NSEvent*> MouseClickInView(NSView* view,
+ NSUInteger clickCount) {
+ const NSRect bounds = [view convertRect:[view bounds] toView:nil];
+ const NSPoint mid_point = NSMakePoint(NSMidX(bounds), NSMidY(bounds));
+ NSEvent* down = MouseEventAtPointInWindow(mid_point, NSLeftMouseDown,
+ [view window], clickCount);
+ NSEvent* up = MouseEventAtPointInWindow(mid_point, NSLeftMouseUp,
+ [view window], clickCount);
+ return std::make_pair(down, up);
+}
+
+} // namespace test_event_utils
diff --git a/chrome/browser/ui/cocoa/theme_install_bubble_view.h b/chrome/browser/ui/cocoa/theme_install_bubble_view.h
new file mode 100644
index 0000000..f8208df
--- /dev/null
+++ b/chrome/browser/ui/cocoa/theme_install_bubble_view.h
@@ -0,0 +1,57 @@
+// 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 "chrome/common/notification_observer.h"
+#include "chrome/common/notification_registrar.h"
+#include "chrome/common/notification_service.h"
+
+@class NSWindow;
+@class ThemeInstallBubbleViewCocoa;
+
+// ThemeInstallBubbleView is a view that provides a "Loading..." bubble in the
+// center of a browser window for use when an extension or theme is loaded.
+// (The Browser class only calls it to install itself into the currently active
+// browser window.) If an extension is being applied, the bubble goes away
+// immediately. If a theme is being applied, it disappears when the theme has
+// been loaded. The purpose of this bubble is to warn the user that the browser
+// may be unresponsive while the theme is being installed.
+//
+// Edge case: note that if one installs a theme in one window and then switches
+// rapidly to another window to install a theme there as well (in the short time
+// between install begin and theme caching seizing the UI thread), the loading
+// bubble will only appear over the first window, as there is only ever one
+// instance of the bubble.
+class ThemeInstallBubbleView : public NotificationObserver {
+ public:
+ ~ThemeInstallBubbleView();
+
+ // NotificationObserver
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+
+ // Show the loading bubble.
+ static void Show(NSWindow* window);
+
+ private:
+ explicit ThemeInstallBubbleView(NSWindow* window);
+
+ // The one copy of the loading bubble.
+ static ThemeInstallBubbleView* view_;
+
+ // A scoped container for notification registries.
+ NotificationRegistrar registrar_;
+
+ // Shut down the popup and remove our notifications.
+ void Close();
+
+ // The actual Cocoa view implementing the bubble.
+ ThemeInstallBubbleViewCocoa* cocoa_view_;
+
+ // Multiple loads can be started at once. Only show one bubble, and keep
+ // track of number of loads happening. Close bubble when num_loads < 1.
+ int num_loads_extant_;
+
+ DISALLOW_COPY_AND_ASSIGN(ThemeInstallBubbleView);
+};
diff --git a/chrome/browser/ui/cocoa/theme_install_bubble_view.mm b/chrome/browser/ui/cocoa/theme_install_bubble_view.mm
new file mode 100644
index 0000000..31c5e81
--- /dev/null
+++ b/chrome/browser/ui/cocoa/theme_install_bubble_view.mm
@@ -0,0 +1,186 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/theme_install_bubble_view.h"
+
+#include "app/l10n_util_mac.h"
+#include "base/scoped_nsobject.h"
+#include "grit/generated_resources.h"
+
+namespace {
+
+// The alpha of the bubble.
+static const float kBubbleAlpha = 0.75;
+
+// The roundedness of the edges of our bubble.
+static const int kBubbleCornerRadius = 4;
+
+// Padding around text in popup box.
+static const int kTextHorizPadding = 90;
+static const int kTextVertPadding = 45;
+
+// Point size of the text in the box.
+static const int kLoadingTextSize = 24;
+
+}
+
+// static
+ThemeInstallBubbleView* ThemeInstallBubbleView::view_ = NULL;
+
+// The Cocoa view to draw a gray rounded rect with "Loading..." in it.
+@interface ThemeInstallBubbleViewCocoa : NSView {
+ @private
+ scoped_nsobject<NSAttributedString> message_;
+
+ NSRect grayRect_;
+ NSRect textRect_;
+}
+
+- (id)init;
+
+// The size of the gray rect that will be drawn.
+- (NSSize)preferredSize;
+// Forces size calculations of where everything will be drawn.
+- (void)layout;
+
+@end
+
+ThemeInstallBubbleView::ThemeInstallBubbleView(NSWindow* window)
+ : cocoa_view_([[ThemeInstallBubbleViewCocoa alloc] init]),
+ num_loads_extant_(1) {
+ DCHECK(window);
+
+ NSView* parent_view = [window contentView];
+ NSRect parent_bounds = [parent_view bounds];
+ if (parent_bounds.size.height < [cocoa_view_ preferredSize].height)
+ Close();
+
+ // Close when theme has been installed.
+ registrar_.Add(
+ this,
+ NotificationType::BROWSER_THEME_CHANGED,
+ NotificationService::AllSources());
+
+ // Close when we are installing an extension, not a theme.
+ registrar_.Add(
+ this,
+ NotificationType::NO_THEME_DETECTED,
+ NotificationService::AllSources());
+ registrar_.Add(
+ this,
+ NotificationType::EXTENSION_INSTALLED,
+ NotificationService::AllSources());
+ registrar_.Add(
+ this,
+ NotificationType::EXTENSION_INSTALL_ERROR,
+ NotificationService::AllSources());
+
+ // Don't let the bubble overlap the confirm dialog.
+ registrar_.Add(
+ this,
+ NotificationType::EXTENSION_WILL_SHOW_CONFIRM_DIALOG,
+ NotificationService::AllSources());
+
+ // Add the view.
+ [cocoa_view_ setFrame:parent_bounds];
+ [cocoa_view_ setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
+ [parent_view addSubview:cocoa_view_
+ positioned:NSWindowAbove
+ relativeTo:nil];
+ [cocoa_view_ layout];
+}
+
+ThemeInstallBubbleView::~ThemeInstallBubbleView() {
+ // Need to delete self; the real work happens in Close().
+}
+
+void ThemeInstallBubbleView::Close() {
+ --num_loads_extant_;
+ if (num_loads_extant_ < 1) {
+ registrar_.RemoveAll();
+ if (cocoa_view_ && [cocoa_view_ superview]) {
+ [cocoa_view_ removeFromSuperview];
+ [cocoa_view_ release];
+ }
+ view_ = NULL;
+ delete this;
+ // this is deleted; nothing more!
+ }
+}
+
+void ThemeInstallBubbleView::Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ Close();
+}
+
+// static
+void ThemeInstallBubbleView::Show(NSWindow* window) {
+ if (view_)
+ ++view_->num_loads_extant_;
+ else
+ view_ = new ThemeInstallBubbleView(window);
+}
+
+@implementation ThemeInstallBubbleViewCocoa
+
+- (id)init {
+ self = [super initWithFrame:NSZeroRect];
+ if (self) {
+ NSString* loadingString =
+ l10n_util::GetNSStringWithFixup(IDS_THEME_LOADING_TITLE);
+ NSFont* loadingFont = [NSFont systemFontOfSize:kLoadingTextSize];
+ NSColor* textColor = [NSColor whiteColor];
+ NSDictionary* loadingAttrs = [NSDictionary dictionaryWithObjectsAndKeys:
+ loadingFont, NSFontAttributeName,
+ textColor, NSForegroundColorAttributeName,
+ nil];
+ message_.reset([[NSAttributedString alloc] initWithString:loadingString
+ attributes:loadingAttrs]);
+
+ // TODO(avi): find a white-on-black spinner
+ }
+ return self;
+}
+
+- (NSSize)preferredSize {
+ NSSize size = [message_.get() size];
+ size.width += kTextHorizPadding;
+ size.height += kTextVertPadding;
+ return size;
+}
+
+// Update the layout to keep the view centered when the window is resized.
+- (void)resizeWithOldSuperviewSize:(NSSize)oldBoundsSize {
+ [super resizeWithOldSuperviewSize:oldBoundsSize];
+ [self layout];
+}
+
+- (void)layout {
+ NSRect bounds = [self bounds];
+
+ grayRect_.size = [self preferredSize];
+ grayRect_.origin.x = (bounds.size.width - grayRect_.size.width) / 2;
+ grayRect_.origin.y = bounds.size.height / 2;
+
+ textRect_.size = [message_.get() size];
+ textRect_.origin.x = (bounds.size.width - [message_.get() size].width) / 2;
+ textRect_.origin.y = (bounds.size.height + kTextVertPadding) / 2;
+}
+
+- (void)drawRect:(NSRect)dirtyRect {
+ [[NSColor clearColor] set];
+ NSRectFillUsingOperation([self bounds], NSCompositeSourceOver);
+
+ [[[NSColor blackColor] colorWithAlphaComponent:kBubbleAlpha] set];
+ [[NSBezierPath bezierPathWithRoundedRect:grayRect_
+ xRadius:kBubbleCornerRadius
+ yRadius:kBubbleCornerRadius] fill];
+
+ [message_.get() drawInRect:textRect_];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/themed_window.h b/chrome/browser/ui/cocoa/themed_window.h
new file mode 100644
index 0000000..d35bfa3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/themed_window.h
@@ -0,0 +1,30 @@
+// 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_UI_COCOA_THEMED_WINDOW_H_
+#define CHROME_BROWSER_UI_COCOA_THEMED_WINDOW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+class ThemeProvider;
+
+// Bit flags; mix-and-match as necessary.
+enum {
+ THEMED_NORMAL = 0,
+ THEMED_INCOGNITO = 1 << 0,
+ THEMED_POPUP = 1 << 1,
+ THEMED_DEVTOOLS = 1 << 2
+};
+typedef NSUInteger ThemedWindowStyle;
+
+// Implemented by windows that support theming.
+
+@interface NSWindow (ThemeProvider)
+- (ThemeProvider*)themeProvider;
+- (ThemedWindowStyle)themedWindowStyle;
+- (NSPoint)themePatternPhase;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_THEMED_WINDOW_H_
diff --git a/chrome/browser/ui/cocoa/themed_window.mm b/chrome/browser/ui/cocoa/themed_window.mm
new file mode 100644
index 0000000..911bf8a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/themed_window.mm
@@ -0,0 +1,23 @@
+// 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/ui/cocoa/themed_window.h"
+
+// Default implementations; used mostly for tests so that the hosting windows
+// don't needs to know about the theming machinery.
+@implementation NSWindow (ThemeProvider)
+
+- (ThemeProvider*)themeProvider {
+ return NULL;
+}
+
+- (ThemedWindowStyle)themedWindowStyle {
+ return THEMED_NORMAL;
+}
+
+- (NSPoint)themePatternPhase {
+ return NSZeroPoint;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/throbber_view.h b/chrome/browser/ui/cocoa/throbber_view.h
new file mode 100644
index 0000000..a680222
--- /dev/null
+++ b/chrome/browser/ui/cocoa/throbber_view.h
@@ -0,0 +1,42 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_THROBBER_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_THROBBER_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+
+@protocol ThrobberDataDelegate;
+
+// A class that knows how to draw an animated state to indicate progress.
+// Creating the class starts the animation, destroying it stops it. There are
+// two types:
+//
+// - Filmstrip: Draws via a sequence of frames in an image. There is no state
+// where the class is frozen on an image and not animating. The image needs to
+// be made of squares such that the height divides evenly into the width.
+//
+// - Toast: Draws an image animating down to the bottom and then another image
+// animating up from the bottom. Stops once the animation is complete.
+
+@interface ThrobberView : NSView {
+ @private
+ id<ThrobberDataDelegate> dataDelegate_;
+}
+
+// Creates a filmstrip view with |frame| and image |image|.
++ (id)filmstripThrobberViewWithFrame:(NSRect)frame
+ image:(NSImage*)image;
+
+// Creates a toast view with |frame| and specified images.
++ (id)toastThrobberViewWithFrame:(NSRect)frame
+ beforeImage:(NSImage*)beforeImage
+ afterImage:(NSImage*)afterImage;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_THROBBER_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/throbber_view.mm b/chrome/browser/ui/cocoa/throbber_view.mm
new file mode 100644
index 0000000..c0e5dd3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/throbber_view.mm
@@ -0,0 +1,372 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/throbber_view.h"
+
+#include <set>
+
+#include "base/logging.h"
+
+static const float kAnimationIntervalSeconds = 0.03; // 30ms, same as windows
+
+@interface ThrobberView(PrivateMethods)
+- (id)initWithFrame:(NSRect)frame delegate:(id<ThrobberDataDelegate>)delegate;
+- (void)maintainTimer;
+- (void)animate;
+@end
+
+@protocol ThrobberDataDelegate <NSObject>
+// Is the current frame the last frame of the animation?
+- (BOOL)animationIsComplete;
+
+// Draw the current frame into the current graphics context.
+- (void)drawFrameInRect:(NSRect)rect;
+
+// Update the frame counter.
+- (void)advanceFrame;
+@end
+
+@interface ThrobberFilmstripDelegate : NSObject
+ <ThrobberDataDelegate> {
+ scoped_nsobject<NSImage> image_;
+ unsigned int numFrames_; // Number of frames in this animation.
+ unsigned int animationFrame_; // Current frame of the animation,
+ // [0..numFrames_)
+}
+
+- (id)initWithImage:(NSImage*)image;
+
+@end
+
+@implementation ThrobberFilmstripDelegate
+
+- (id)initWithImage:(NSImage*)image {
+ if ((self = [super init])) {
+ // Reset the animation counter so there's no chance we are off the end.
+ animationFrame_ = 0;
+
+ // Ensure that the height divides evenly into the width. Cache the
+ // number of frames in the animation for later.
+ NSSize imageSize = [image size];
+ DCHECK(imageSize.height && imageSize.width);
+ if (!imageSize.height)
+ return nil;
+ DCHECK((int)imageSize.width % (int)imageSize.height == 0);
+ numFrames_ = (int)imageSize.width / (int)imageSize.height;
+ DCHECK(numFrames_);
+ image_.reset([image retain]);
+ }
+ return self;
+}
+
+- (BOOL)animationIsComplete {
+ return NO;
+}
+
+- (void)drawFrameInRect:(NSRect)rect {
+ float imageDimension = [image_ size].height;
+ float xOffset = animationFrame_ * imageDimension;
+ NSRect sourceImageRect =
+ NSMakeRect(xOffset, 0, imageDimension, imageDimension);
+ [image_ drawInRect:rect
+ fromRect:sourceImageRect
+ operation:NSCompositeSourceOver
+ fraction:1.0];
+}
+
+- (void)advanceFrame {
+ animationFrame_ = ++animationFrame_ % numFrames_;
+}
+
+@end
+
+@interface ThrobberToastDelegate : NSObject
+ <ThrobberDataDelegate> {
+ scoped_nsobject<NSImage> image1_;
+ scoped_nsobject<NSImage> image2_;
+ NSSize image1Size_;
+ NSSize image2Size_;
+ int animationFrame_; // Current frame of the animation,
+}
+
+- (id)initWithImage1:(NSImage*)image1 image2:(NSImage*)image2;
+
+@end
+
+@implementation ThrobberToastDelegate
+
+- (id)initWithImage1:(NSImage*)image1 image2:(NSImage*)image2 {
+ if ((self = [super init])) {
+ image1_.reset([image1 retain]);
+ image2_.reset([image2 retain]);
+ image1Size_ = [image1 size];
+ image2Size_ = [image2 size];
+ animationFrame_ = 0;
+ }
+ return self;
+}
+
+- (BOOL)animationIsComplete {
+ if (animationFrame_ >= image1Size_.height + image2Size_.height)
+ return YES;
+
+ return NO;
+}
+
+// From [0..image1Height) we draw image1, at image1Height we draw nothing, and
+// from [image1Height+1..image1Hight+image2Height] we draw the second image.
+- (void)drawFrameInRect:(NSRect)rect {
+ NSImage* image = nil;
+ NSSize srcSize;
+ NSRect destRect;
+
+ if (animationFrame_ < image1Size_.height) {
+ image = image1_.get();
+ srcSize = image1Size_;
+ destRect = NSMakeRect(0, -animationFrame_,
+ image1Size_.width, image1Size_.height);
+ } else if (animationFrame_ == image1Size_.height) {
+ // nothing; intermediate blank frame
+ } else {
+ image = image2_.get();
+ srcSize = image2Size_;
+ destRect = NSMakeRect(0, animationFrame_ -
+ (image1Size_.height + image2Size_.height),
+ image2Size_.width, image2Size_.height);
+ }
+
+ if (image) {
+ NSRect sourceImageRect =
+ NSMakeRect(0, 0, srcSize.width, srcSize.height);
+ [image drawInRect:destRect
+ fromRect:sourceImageRect
+ operation:NSCompositeSourceOver
+ fraction:1.0];
+ }
+}
+
+- (void)advanceFrame {
+ ++animationFrame_;
+}
+
+@end
+
+typedef std::set<ThrobberView*> ThrobberSet;
+
+// ThrobberTimer manages the animation of a set of ThrobberViews. It allows
+// a single timer instance to be shared among as many ThrobberViews as needed.
+@interface ThrobberTimer : NSObject {
+ @private
+ // A set of weak references to each ThrobberView that should be notified
+ // whenever the timer fires.
+ ThrobberSet throbbers_;
+
+ // Weak reference to the timer that calls back to this object. The timer
+ // retains this object.
+ NSTimer* timer_;
+
+ // Whether the timer is actively running. To avoid timer construction
+ // and destruction overhead, the timer is not invalidated when it is not
+ // needed, but its next-fire date is set to [NSDate distantFuture].
+ // It is not possible to determine whether the timer has been suspended by
+ // comparing its fireDate to [NSDate distantFuture], though, so a separate
+ // variable is used to track this state.
+ BOOL timerRunning_;
+
+ // The thread that created this object. Used to validate that ThrobberViews
+ // are only added and removed on the same thread that the fire action will
+ // be performed on.
+ NSThread* validThread_;
+}
+
+// Returns a shared ThrobberTimer. Everyone is expected to use the same
+// instance.
++ (ThrobberTimer*)sharedThrobberTimer;
+
+// Invalidates the timer, which will cause it to remove itself from the run
+// loop. This causes the timer to be released, and it should then release
+// this object.
+- (void)invalidate;
+
+// Adds or removes ThrobberView objects from the throbbers_ set.
+- (void)addThrobber:(ThrobberView*)throbber;
+- (void)removeThrobber:(ThrobberView*)throbber;
+@end
+
+@interface ThrobberTimer(PrivateMethods)
+// Starts or stops the timer as needed as ThrobberViews are added and removed
+// from the throbbers_ set.
+- (void)maintainTimer;
+
+// Calls animate on each ThrobberView in the throbbers_ set.
+- (void)fire:(NSTimer*)timer;
+@end
+
+@implementation ThrobberTimer
+- (id)init {
+ if ((self = [super init])) {
+ // Start out with a timer that fires at the appropriate interval, but
+ // prevent it from firing by setting its next-fire date to the distant
+ // future. Once a ThrobberView is added, the timer will be allowed to
+ // start firing.
+ timer_ = [NSTimer scheduledTimerWithTimeInterval:kAnimationIntervalSeconds
+ target:self
+ selector:@selector(fire:)
+ userInfo:nil
+ repeats:YES];
+ [timer_ setFireDate:[NSDate distantFuture]];
+ timerRunning_ = NO;
+
+ validThread_ = [NSThread currentThread];
+ }
+ return self;
+}
+
++ (ThrobberTimer*)sharedThrobberTimer {
+ // Leaked. That's OK, it's scoped to the lifetime of the application.
+ static ThrobberTimer* sharedInstance = [[ThrobberTimer alloc] init];
+ return sharedInstance;
+}
+
+- (void)invalidate {
+ [timer_ invalidate];
+}
+
+- (void)addThrobber:(ThrobberView*)throbber {
+ DCHECK([NSThread currentThread] == validThread_);
+ throbbers_.insert(throbber);
+ [self maintainTimer];
+}
+
+- (void)removeThrobber:(ThrobberView*)throbber {
+ DCHECK([NSThread currentThread] == validThread_);
+ throbbers_.erase(throbber);
+ [self maintainTimer];
+}
+
+- (void)maintainTimer {
+ BOOL oldRunning = timerRunning_;
+ BOOL newRunning = throbbers_.empty() ? NO : YES;
+
+ if (oldRunning == newRunning)
+ return;
+
+ // To start the timer, set its next-fire date to an appropriate interval from
+ // now. To suspend the timer, set its next-fire date to a preposterous time
+ // in the future.
+ NSDate* fireDate;
+ if (newRunning)
+ fireDate = [NSDate dateWithTimeIntervalSinceNow:kAnimationIntervalSeconds];
+ else
+ fireDate = [NSDate distantFuture];
+
+ [timer_ setFireDate:fireDate];
+ timerRunning_ = newRunning;
+}
+
+- (void)fire:(NSTimer*)timer {
+ // The call to [throbber animate] may result in the ThrobberView calling
+ // removeThrobber: if it decides it's done animating. That would invalidate
+ // the iterator, making it impossible to correctly get to the next element
+ // in the set. To prevent that from happening, a second iterator is used
+ // and incremented before calling [throbber animate].
+ ThrobberSet::const_iterator current = throbbers_.begin();
+ ThrobberSet::const_iterator next = current;
+ while (current != throbbers_.end()) {
+ ++next;
+ ThrobberView* throbber = *current;
+ [throbber animate];
+ current = next;
+ }
+}
+@end
+
+@implementation ThrobberView
+
++ (id)filmstripThrobberViewWithFrame:(NSRect)frame
+ image:(NSImage*)image {
+ ThrobberFilmstripDelegate* delegate =
+ [[[ThrobberFilmstripDelegate alloc] initWithImage:image] autorelease];
+ if (!delegate)
+ return nil;
+
+ return [[[ThrobberView alloc] initWithFrame:frame
+ delegate:delegate] autorelease];
+}
+
++ (id)toastThrobberViewWithFrame:(NSRect)frame
+ beforeImage:(NSImage*)beforeImage
+ afterImage:(NSImage*)afterImage {
+ ThrobberToastDelegate* delegate =
+ [[[ThrobberToastDelegate alloc] initWithImage1:beforeImage
+ image2:afterImage] autorelease];
+ if (!delegate)
+ return nil;
+
+ return [[[ThrobberView alloc] initWithFrame:frame
+ delegate:delegate] autorelease];
+}
+
+- (id)initWithFrame:(NSRect)frame delegate:(id<ThrobberDataDelegate>)delegate {
+ if ((self = [super initWithFrame:frame])) {
+ dataDelegate_ = [delegate retain];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [dataDelegate_ release];
+ [[ThrobberTimer sharedThrobberTimer] removeThrobber:self];
+
+ [super dealloc];
+}
+
+// Manages this ThrobberView's membership in the shared throbber timer set on
+// the basis of its visibility and whether its animation needs to continue
+// running.
+- (void)maintainTimer {
+ ThrobberTimer* throbberTimer = [ThrobberTimer sharedThrobberTimer];
+
+ if ([self window] && ![self isHidden] && ![dataDelegate_ animationIsComplete])
+ [throbberTimer addThrobber:self];
+ else
+ [throbberTimer removeThrobber:self];
+}
+
+// A ThrobberView added to a window may need to begin animating; a ThrobberView
+// removed from a window should stop.
+- (void)viewDidMoveToWindow {
+ [self maintainTimer];
+ [super viewDidMoveToWindow];
+}
+
+// A hidden ThrobberView should stop animating.
+- (void)viewDidHide {
+ [self maintainTimer];
+ [super viewDidHide];
+}
+
+// A visible ThrobberView may need to start animating.
+- (void)viewDidUnhide {
+ [self maintainTimer];
+ [super viewDidUnhide];
+}
+
+// Called when the timer fires. Advance the frame, dirty the display, and remove
+// the throbber if it's no longer needed.
+- (void)animate {
+ [dataDelegate_ advanceFrame];
+ [self setNeedsDisplay:YES];
+
+ if ([dataDelegate_ animationIsComplete]) {
+ [[ThrobberTimer sharedThrobberTimer] removeThrobber:self];
+ }
+}
+
+// Overridden to draw the appropriate frame in the image strip.
+- (void)drawRect:(NSRect)rect {
+ [dataDelegate_ drawFrameInRect:[self bounds]];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/throbber_view_unittest.mm b/chrome/browser/ui/cocoa/throbber_view_unittest.mm
new file mode 100644
index 0000000..b1a2ce4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/throbber_view_unittest.mm
@@ -0,0 +1,32 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "app/resource_bundle.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/throbber_view.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+#include "grit/app_resources.h"
+
+namespace {
+
+class ThrobberViewTest : public CocoaTest {
+ public:
+ ThrobberViewTest() {
+ NSRect frame = NSMakeRect(10, 10, 16, 16);
+ NSImage* image =
+ ResourceBundle::GetSharedInstance().GetNativeImageNamed(IDR_THROBBER);
+ view_ = [ThrobberView filmstripThrobberViewWithFrame:frame image:image];
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ ThrobberView* view_;
+};
+
+TEST_VIEW(ThrobberViewTest, view_)
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/toolbar_controller.h b/chrome/browser/ui/cocoa/toolbar_controller.h
new file mode 100644
index 0000000..029f89d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/toolbar_controller.h
@@ -0,0 +1,189 @@
+// 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_UI_COCOA_TOOLBAR_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_TOOLBAR_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_ptr.h"
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/command_observer_bridge.h"
+#import "chrome/browser/ui/cocoa/delayedmenu_button.h"
+#import "chrome/browser/ui/cocoa/url_drop_target.h"
+#import "chrome/browser/ui/cocoa/view_resizer.h"
+#include "chrome/browser/prefs/pref_member.h"
+
+@class AutocompleteTextField;
+@class AutocompleteTextFieldEditor;
+@class BrowserActionsContainerView;
+@class BackForwardMenuController;
+class Browser;
+@class BrowserActionsController;
+class CommandUpdater;
+@class DelayedMenuButton;
+class LocationBar;
+class LocationBarViewMac;
+@class MenuButton;
+namespace ToolbarControllerInternal {
+class NotificationBridge;
+class WrenchAcceleratorDelegate;
+} // namespace ToolbarControllerInternal
+class Profile;
+@class ReloadButton;
+class TabContents;
+class ToolbarModel;
+@class WrenchMenuController;
+class WrenchMenuModel;
+
+// A controller for the toolbar in the browser window. Manages
+// updating the state for location bar and back/fwd/reload/go buttons.
+// Manages the bookmark bar and its position in the window relative to
+// the web content view.
+
+@interface ToolbarController : NSViewController<CommandObserverProtocol,
+ URLDropTargetController> {
+ @protected
+ // The ordering is important for unit tests. If new items are added or the
+ // ordering is changed, make sure to update |-toolbarViews| and the
+ // corresponding enum in the unit tests.
+ IBOutlet DelayedMenuButton* backButton_;
+ IBOutlet DelayedMenuButton* forwardButton_;
+ IBOutlet ReloadButton* reloadButton_;
+ IBOutlet NSButton* homeButton_;
+ IBOutlet MenuButton* wrenchButton_;
+ IBOutlet AutocompleteTextField* locationBar_;
+ IBOutlet BrowserActionsContainerView* browserActionsContainerView_;
+ IBOutlet WrenchMenuController* wrenchMenuController_;
+
+ @private
+ ToolbarModel* toolbarModel_; // weak, one per window
+ CommandUpdater* commands_; // weak, one per window
+ Profile* profile_; // weak, one per window
+ Browser* browser_; // weak, one per window
+ scoped_ptr<CommandObserverBridge> commandObserver_;
+ scoped_ptr<LocationBarViewMac> locationBarView_;
+ scoped_nsobject<AutocompleteTextFieldEditor> autocompleteTextFieldEditor_;
+ id<ViewResizer> resizeDelegate_; // weak
+ scoped_nsobject<BackForwardMenuController> backMenuController_;
+ scoped_nsobject<BackForwardMenuController> forwardMenuController_;
+ scoped_nsobject<BrowserActionsController> browserActionsController_;
+
+ // Lazily-instantiated model and delegate for the menu on the
+ // wrench button. Once visible, it will be non-null, but will not
+ // reaped when the menu is hidden once it is initially shown.
+ scoped_ptr<ToolbarControllerInternal::WrenchAcceleratorDelegate>
+ acceleratorDelegate_;
+ scoped_ptr<WrenchMenuModel> wrenchMenuModel_;
+
+ // Used for monitoring the optional toolbar button prefs.
+ scoped_ptr<ToolbarControllerInternal::NotificationBridge> notificationBridge_;
+ BooleanPrefMember showHomeButton_;
+ BooleanPrefMember showPageOptionButtons_;
+ BOOL hasToolbar_; // If NO, we may have only the location bar.
+ BOOL hasLocationBar_; // If |hasToolbar_| is YES, this must also be YES.
+ BOOL locationBarAtMinSize_; // If the location bar is at the minimum size.
+
+ // We have an extra retain in the locationBar_.
+ // See comments in awakeFromNib for more info.
+ scoped_nsobject<AutocompleteTextField> locationBarRetainer_;
+
+ // Tracking area for mouse enter/exit/moved in the toolbar.
+ scoped_nsobject<NSTrackingArea> trackingArea_;
+
+ // We retain/release the hover button since interaction with the
+ // button may make it go away (e.g. delete menu option over a
+ // bookmark button). Thus this variable is not weak. The
+ // hoveredButton_ is required to have an NSCell that responds to
+ // setMouseInside:animate:.
+ NSButton* hoveredButton_;
+}
+
+// Initialize the toolbar and register for command updates. The profile is
+// needed for initializing the location bar. The browser is needed for
+// initializing the back/forward menus.
+- (id)initWithModel:(ToolbarModel*)model
+ commands:(CommandUpdater*)commands
+ profile:(Profile*)profile
+ browser:(Browser*)browser
+ resizeDelegate:(id<ViewResizer>)resizeDelegate;
+
+// Get the C++ bridge object representing the location bar for this tab.
+- (LocationBarViewMac*)locationBarBridge;
+
+// Called by the Window delegate so we can provide a custom field editor if
+// needed.
+// Note that this may be called for objects unrelated to the toolbar.
+// returns nil if we don't want to override the custom field editor for |obj|.
+- (id)customFieldEditorForObject:(id)obj;
+
+// Make the location bar the first responder, if possible.
+- (void)focusLocationBar:(BOOL)selectAll;
+
+// Updates the toolbar (and transitively the location bar) with the states of
+// the specified |tab|. If |shouldRestore| is true, we're switching
+// (back?) to this tab and should restore any previous location bar state
+// (such as user editing) as well.
+- (void)updateToolbarWithContents:(TabContents*)tabForRestoring
+ shouldRestoreState:(BOOL)shouldRestore;
+
+// Sets whether or not the current page in the frontmost tab is bookmarked.
+- (void)setStarredState:(BOOL)isStarred;
+
+// Called to update the loading state. Handles updating the go/stop
+// button state. |force| is set if the update is due to changing
+// tabs, as opposed to the page-load finishing. See comment in
+// reload_button.h.
+- (void)setIsLoading:(BOOL)isLoading force:(BOOL)force;
+
+// Allow turning off the toolbar (but we may keep the location bar without a
+// surrounding toolbar). If |toolbar| is YES, the value of |hasLocationBar| is
+// ignored. This changes the behavior of other methods, like |-view|.
+- (void)setHasToolbar:(BOOL)toolbar hasLocationBar:(BOOL)locBar;
+
+// Point on the star icon for the bookmark bubble to be - in the
+// associated window's coordinate system.
+- (NSPoint)bookmarkBubblePoint;
+
+// Returns the desired toolbar height for the given compression factor.
+- (CGFloat)desiredHeightForCompression:(CGFloat)compressByHeight;
+
+// Set the opacity of the divider (the line at the bottom) *if* we have a
+// |ToolbarView| (0 means don't show it); no-op otherwise.
+- (void)setDividerOpacity:(CGFloat)opacity;
+
+// Create and add the Browser Action buttons to the toolbar view.
+- (void)createBrowserActionButtons;
+
+// Return the BrowserActionsController for this toolbar.
+- (BrowserActionsController*)browserActionsController;
+
+@end
+
+// A set of private methods used by subclasses. Do not call these directly
+// unless a subclass of ToolbarController.
+@interface ToolbarController(ProtectedMethods)
+// Designated initializer which takes a nib name in order to allow subclasses
+// to load a different nib file.
+- (id)initWithModel:(ToolbarModel*)model
+ commands:(CommandUpdater*)commands
+ profile:(Profile*)profile
+ browser:(Browser*)browser
+ resizeDelegate:(id<ViewResizer>)resizeDelegate
+ nibFileNamed:(NSString*)nibName;
+@end
+
+// A set of private methods used by tests, in the absence of "friends" in ObjC.
+@interface ToolbarController(PrivateTestMethods)
+// Returns an array of views in the order of the outlets above.
+- (NSArray*)toolbarViews;
+- (void)showOptionalHomeButton;
+- (void)installWrenchMenu;
+- (WrenchMenuController*)wrenchMenuController;
+// Return a hover button for the current event.
+- (NSButton*)hoverButtonForEvent:(NSEvent*)theEvent;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_TOOLBAR_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/toolbar_controller.mm b/chrome/browser/ui/cocoa/toolbar_controller.mm
new file mode 100644
index 0000000..aa1d521
--- /dev/null
+++ b/chrome/browser/ui/cocoa/toolbar_controller.mm
@@ -0,0 +1,753 @@
+// 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/ui/cocoa/toolbar_controller.h"
+
+#include <algorithm>
+
+#include "app/l10n_util.h"
+#include "app/l10n_util_mac.h"
+#include "app/menus/accelerator_cocoa.h"
+#include "app/menus/menu_model.h"
+#include "base/mac_util.h"
+#include "base/nsimage_cache_mac.h"
+#include "base/singleton.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/autocomplete/autocomplete_edit_view.h"
+#include "chrome/browser/net/url_fixer_upper.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/search_engines/template_url_model.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/themes/browser_theme_provider.h"
+#include "chrome/browser/toolbar_model.h"
+#include "chrome/browser/upgrade_detector.h"
+#include "chrome/browser/wrench_menu_model.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_window.h"
+#import "chrome/browser/ui/cocoa/accelerators_cocoa.h"
+#import "chrome/browser/ui/cocoa/back_forward_menu_controller.h"
+#import "chrome/browser/ui/cocoa/background_gradient_view.h"
+#import "chrome/browser/ui/cocoa/encoding_menu_controller_delegate_mac.h"
+#import "chrome/browser/ui/cocoa/extensions/browser_action_button.h"
+#import "chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h"
+#import "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h"
+#import "chrome/browser/ui/cocoa/gradient_button_cell.h"
+#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
+#import "chrome/browser/ui/cocoa/menu_button.h"
+#import "chrome/browser/ui/cocoa/menu_controller.h"
+#import "chrome/browser/ui/cocoa/reload_button.h"
+#import "chrome/browser/ui/cocoa/toolbar_view.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+#import "chrome/browser/ui/cocoa/wrench_menu_controller.h"
+#include "chrome/common/notification_details.h"
+#include "chrome/common/notification_observer.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/notification_type.h"
+#include "chrome/common/pref_names.h"
+#include "gfx/rect.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+
+namespace {
+
+// Names of images in the bundle for buttons.
+NSString* const kBackButtonImageName = @"back_Template.pdf";
+NSString* const kForwardButtonImageName = @"forward_Template.pdf";
+NSString* const kReloadButtonReloadImageName = @"reload_Template.pdf";
+NSString* const kReloadButtonStopImageName = @"stop_Template.pdf";
+NSString* const kHomeButtonImageName = @"home_Template.pdf";
+NSString* const kWrenchButtonImageName = @"tools_Template.pdf";
+
+// Height of the toolbar in pixels when the bookmark bar is closed.
+const CGFloat kBaseToolbarHeight = 35.0;
+
+// The minimum width of the location bar in pixels.
+const CGFloat kMinimumLocationBarWidth = 100.0;
+
+// The duration of any animation that occurs within the toolbar in seconds.
+const CGFloat kAnimationDuration = 0.2;
+
+// The amount of left padding that the wrench menu should have.
+const CGFloat kWrenchMenuLeftPadding = 3.0;
+
+} // namespace
+
+@interface ToolbarController(Private)
+- (void)addAccessibilityDescriptions;
+- (void)initCommandStatus:(CommandUpdater*)commands;
+- (void)prefChanged:(std::string*)prefName;
+- (BackgroundGradientView*)backgroundGradientView;
+- (void)toolbarFrameChanged;
+- (void)pinLocationBarToLeftOfBrowserActionsContainerAndAnimate:(BOOL)animate;
+- (void)maintainMinimumLocationBarWidth;
+- (void)adjustBrowserActionsContainerForNewWindow:(NSNotification*)notification;
+- (void)browserActionsContainerDragged:(NSNotification*)notification;
+- (void)browserActionsContainerDragFinished:(NSNotification*)notification;
+- (void)browserActionsVisibilityChanged:(NSNotification*)notification;
+- (void)adjustLocationSizeBy:(CGFloat)dX animate:(BOOL)animate;
+- (void)badgeWrenchMenu;
+@end
+
+namespace ToolbarControllerInternal {
+
+// A C++ delegate that handles the accelerators in the wrench menu.
+class WrenchAcceleratorDelegate : public menus::AcceleratorProvider {
+ public:
+ virtual bool GetAcceleratorForCommandId(int command_id,
+ menus::Accelerator* accelerator_generic) {
+ // Downcast so that when the copy constructor is invoked below, the key
+ // string gets copied, too.
+ menus::AcceleratorCocoa* out_accelerator =
+ static_cast<menus::AcceleratorCocoa*>(accelerator_generic);
+ AcceleratorsCocoa* keymap = Singleton<AcceleratorsCocoa>::get();
+ const menus::AcceleratorCocoa* accelerator =
+ keymap->GetAcceleratorForCommand(command_id);
+ if (accelerator) {
+ *out_accelerator = *accelerator;
+ return true;
+ }
+ return false;
+ }
+};
+
+// A class registered for C++ notifications. This is used to detect changes in
+// preferences and upgrade available notifications. Bridges the notification
+// back to the ToolbarController.
+class NotificationBridge : public NotificationObserver {
+ public:
+ explicit NotificationBridge(ToolbarController* controller)
+ : controller_(controller) {
+ registrar_.Add(this, NotificationType::UPGRADE_RECOMMENDED,
+ NotificationService::AllSources());
+ }
+
+ // Overridden from NotificationObserver:
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ if (type == NotificationType::PREF_CHANGED)
+ [controller_ prefChanged:Details<std::string>(details).ptr()];
+ else if (type == NotificationType::UPGRADE_RECOMMENDED)
+ [controller_ badgeWrenchMenu];
+ }
+
+ private:
+ ToolbarController* controller_; // weak, owns us
+
+ NotificationRegistrar registrar_;
+};
+
+} // namespace ToolbarControllerInternal
+
+@implementation ToolbarController
+
+- (id)initWithModel:(ToolbarModel*)model
+ commands:(CommandUpdater*)commands
+ profile:(Profile*)profile
+ browser:(Browser*)browser
+ resizeDelegate:(id<ViewResizer>)resizeDelegate
+ nibFileNamed:(NSString*)nibName {
+ DCHECK(model && commands && profile && [nibName length]);
+ if ((self = [super initWithNibName:nibName
+ bundle:mac_util::MainAppBundle()])) {
+ toolbarModel_ = model;
+ commands_ = commands;
+ profile_ = profile;
+ browser_ = browser;
+ resizeDelegate_ = resizeDelegate;
+ hasToolbar_ = YES;
+ hasLocationBar_ = YES;
+
+ // Register for notifications about state changes for the toolbar buttons
+ commandObserver_.reset(new CommandObserverBridge(self, commands));
+ commandObserver_->ObserveCommand(IDC_BACK);
+ commandObserver_->ObserveCommand(IDC_FORWARD);
+ commandObserver_->ObserveCommand(IDC_RELOAD);
+ commandObserver_->ObserveCommand(IDC_HOME);
+ commandObserver_->ObserveCommand(IDC_BOOKMARK_PAGE);
+ }
+ return self;
+}
+
+- (id)initWithModel:(ToolbarModel*)model
+ commands:(CommandUpdater*)commands
+ profile:(Profile*)profile
+ browser:(Browser*)browser
+ resizeDelegate:(id<ViewResizer>)resizeDelegate {
+ if ((self = [self initWithModel:model
+ commands:commands
+ profile:profile
+ browser:browser
+ resizeDelegate:resizeDelegate
+ nibFileNamed:@"Toolbar"])) {
+ }
+ return self;
+}
+
+
+- (void)dealloc {
+ // Unset ViewIDs of toolbar elements.
+ // ViewIDs of |toolbarView|, |reloadButton_|, |locationBar_| and
+ // |browserActionsContainerView_| are handled by themselves.
+ view_id_util::UnsetID(backButton_);
+ view_id_util::UnsetID(forwardButton_);
+ view_id_util::UnsetID(homeButton_);
+ view_id_util::UnsetID(wrenchButton_);
+
+ // Make sure any code in the base class which assumes [self view] is
+ // the "parent" view continues to work.
+ hasToolbar_ = YES;
+ hasLocationBar_ = YES;
+
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+
+ if (trackingArea_.get())
+ [[self view] removeTrackingArea:trackingArea_.get()];
+ [super dealloc];
+}
+
+// Called after the view is done loading and the outlets have been hooked up.
+// Now we can hook up bridges that rely on UI objects such as the location
+// bar and button state.
+- (void)awakeFromNib {
+ // A bug in AppKit (<rdar://7298597>, <http://openradar.me/7298597>) causes
+ // images loaded directly from nibs in a framework to not get their "template"
+ // flags set properly. Thus, despite the images being set on the buttons in
+ // the xib, we must set them in code.
+ [backButton_ setImage:nsimage_cache::ImageNamed(kBackButtonImageName)];
+ [forwardButton_ setImage:nsimage_cache::ImageNamed(kForwardButtonImageName)];
+ [reloadButton_
+ setImage:nsimage_cache::ImageNamed(kReloadButtonReloadImageName)];
+ [homeButton_ setImage:nsimage_cache::ImageNamed(kHomeButtonImageName)];
+ [wrenchButton_ setImage:nsimage_cache::ImageNamed(kWrenchButtonImageName)];
+
+ if (Singleton<UpgradeDetector>::get()->notify_upgrade())
+ [self badgeWrenchMenu];
+
+ [backButton_ setShowsBorderOnlyWhileMouseInside:YES];
+ [forwardButton_ setShowsBorderOnlyWhileMouseInside:YES];
+ [reloadButton_ setShowsBorderOnlyWhileMouseInside:YES];
+ [homeButton_ setShowsBorderOnlyWhileMouseInside:YES];
+ [wrenchButton_ setShowsBorderOnlyWhileMouseInside:YES];
+
+ [self initCommandStatus:commands_];
+ locationBarView_.reset(new LocationBarViewMac(locationBar_,
+ commands_, toolbarModel_,
+ profile_, browser_));
+ [locationBar_ setFont:[NSFont systemFontOfSize:[NSFont systemFontSize]]];
+ // Register pref observers for the optional home and page/options buttons
+ // and then add them to the toolbar based on those prefs.
+ notificationBridge_.reset(
+ new ToolbarControllerInternal::NotificationBridge(self));
+ PrefService* prefs = profile_->GetPrefs();
+ showHomeButton_.Init(prefs::kShowHomeButton, prefs,
+ notificationBridge_.get());
+ showPageOptionButtons_.Init(prefs::kShowPageOptionsButtons, prefs,
+ notificationBridge_.get());
+ [self showOptionalHomeButton];
+ [self installWrenchMenu];
+
+ // Create the controllers for the back/forward menus.
+ backMenuController_.reset([[BackForwardMenuController alloc]
+ initWithBrowser:browser_
+ modelType:BACK_FORWARD_MENU_TYPE_BACK
+ button:backButton_]);
+ forwardMenuController_.reset([[BackForwardMenuController alloc]
+ initWithBrowser:browser_
+ modelType:BACK_FORWARD_MENU_TYPE_FORWARD
+ button:forwardButton_]);
+
+ // For a popup window, the toolbar is really just a location bar
+ // (see override for [ToolbarController view], below). When going
+ // fullscreen, we remove the toolbar controller's view from the view
+ // hierarchy. Calling [locationBar_ removeFromSuperview] when going
+ // fullscreen causes it to get released, making us unhappy
+ // (http://crbug.com/18551). We avoid the problem by incrementing
+ // the retain count of the location bar; use of the scoped object
+ // helps us remember to release it.
+ locationBarRetainer_.reset([locationBar_ retain]);
+ trackingArea_.reset(
+ [[NSTrackingArea alloc] initWithRect:NSZeroRect // Ignored
+ options:NSTrackingMouseMoved |
+ NSTrackingInVisibleRect |
+ NSTrackingMouseEnteredAndExited |
+ NSTrackingActiveAlways
+ owner:self
+ userInfo:nil]);
+ NSView* toolbarView = [self view];
+ [toolbarView addTrackingArea:trackingArea_.get()];
+
+ // If the user has any Browser Actions installed, the container view for them
+ // may have to be resized depending on the width of the toolbar frame.
+ [toolbarView setPostsFrameChangedNotifications:YES];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(toolbarFrameChanged)
+ name:NSViewFrameDidChangeNotification
+ object:toolbarView];
+
+ // Set ViewIDs for toolbar elements which don't have their dedicated class.
+ // ViewIDs of |toolbarView|, |reloadButton_|, |locationBar_| and
+ // |browserActionsContainerView_| are handled by themselves.
+ view_id_util::SetID(backButton_, VIEW_ID_BACK_BUTTON);
+ view_id_util::SetID(forwardButton_, VIEW_ID_FORWARD_BUTTON);
+ view_id_util::SetID(homeButton_, VIEW_ID_HOME_BUTTON);
+ view_id_util::SetID(wrenchButton_, VIEW_ID_APP_MENU);
+
+ [self addAccessibilityDescriptions];
+}
+
+- (void)addAccessibilityDescriptions {
+ // Set accessibility descriptions. http://openradar.appspot.com/7496255
+ NSString* description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_BACK);
+ [[backButton_ cell]
+ accessibilitySetOverrideValue:description
+ forAttribute:NSAccessibilityDescriptionAttribute];
+ description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_FORWARD);
+ [[forwardButton_ cell]
+ accessibilitySetOverrideValue:description
+ forAttribute:NSAccessibilityDescriptionAttribute];
+ description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_RELOAD);
+ [[reloadButton_ cell]
+ accessibilitySetOverrideValue:description
+ forAttribute:NSAccessibilityDescriptionAttribute];
+ description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_HOME);
+ [[homeButton_ cell]
+ accessibilitySetOverrideValue:description
+ forAttribute:NSAccessibilityDescriptionAttribute];
+ description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_LOCATION);
+ [[locationBar_ cell]
+ accessibilitySetOverrideValue:description
+ forAttribute:NSAccessibilityDescriptionAttribute];
+ description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_APP);
+ [[wrenchButton_ cell]
+ accessibilitySetOverrideValue:description
+ forAttribute:NSAccessibilityDescriptionAttribute];
+}
+
+- (void)mouseExited:(NSEvent*)theEvent {
+ [[hoveredButton_ cell] setMouseInside:NO animate:YES];
+ [hoveredButton_ release];
+ hoveredButton_ = nil;
+}
+
+- (NSButton*)hoverButtonForEvent:(NSEvent*)theEvent {
+ NSButton* targetView = (NSButton*)[[self view]
+ hitTest:[theEvent locationInWindow]];
+
+ // Only interpret the view as a hoverButton_ if it's both button and has a
+ // button cell that cares. GradientButtonCell derived cells care.
+ if (([targetView isKindOfClass:[NSButton class]]) &&
+ ([[targetView cell]
+ respondsToSelector:@selector(setMouseInside:animate:)]))
+ return targetView;
+ return nil;
+}
+
+- (void)mouseMoved:(NSEvent*)theEvent {
+ NSButton* targetView = [self hoverButtonForEvent:theEvent];
+ if (hoveredButton_ != targetView) {
+ [[hoveredButton_ cell] setMouseInside:NO animate:YES];
+ [[targetView cell] setMouseInside:YES animate:YES];
+ [hoveredButton_ release];
+ hoveredButton_ = [targetView retain];
+ }
+}
+
+- (void)mouseEntered:(NSEvent*)event {
+ [self mouseMoved:event];
+}
+
+- (LocationBarViewMac*)locationBarBridge {
+ return locationBarView_.get();
+}
+
+- (void)focusLocationBar:(BOOL)selectAll {
+ if (locationBarView_.get())
+ locationBarView_->FocusLocation(selectAll ? true : false);
+}
+
+// Called when the state for a command changes to |enabled|. Update the
+// corresponding UI element.
+- (void)enabledStateChangedForCommand:(NSInteger)command enabled:(BOOL)enabled {
+ NSButton* button = nil;
+ switch (command) {
+ case IDC_BACK:
+ button = backButton_;
+ break;
+ case IDC_FORWARD:
+ button = forwardButton_;
+ break;
+ case IDC_HOME:
+ button = homeButton_;
+ break;
+ }
+ [button setEnabled:enabled];
+}
+
+// Init the enabled state of the buttons on the toolbar to match the state in
+// the controller.
+- (void)initCommandStatus:(CommandUpdater*)commands {
+ [backButton_ setEnabled:commands->IsCommandEnabled(IDC_BACK) ? YES : NO];
+ [forwardButton_
+ setEnabled:commands->IsCommandEnabled(IDC_FORWARD) ? YES : NO];
+ [reloadButton_ setEnabled:YES];
+ [homeButton_ setEnabled:commands->IsCommandEnabled(IDC_HOME) ? YES : NO];
+}
+
+- (void)updateToolbarWithContents:(TabContents*)tab
+ shouldRestoreState:(BOOL)shouldRestore {
+ locationBarView_->Update(tab, shouldRestore ? true : false);
+
+ [locationBar_ updateCursorAndToolTipRects];
+
+ if (browserActionsController_.get()) {
+ [browserActionsController_ update];
+ }
+}
+
+- (void)setStarredState:(BOOL)isStarred {
+ locationBarView_->SetStarred(isStarred ? true : false);
+}
+
+- (void)setIsLoading:(BOOL)isLoading force:(BOOL)force {
+ [reloadButton_ setIsLoading:isLoading force:force];
+}
+
+- (void)setHasToolbar:(BOOL)toolbar hasLocationBar:(BOOL)locBar {
+ [self view]; // Force nib loading.
+
+ hasToolbar_ = toolbar;
+
+ // If there's a toolbar, there must be a location bar.
+ DCHECK((toolbar && locBar) || !toolbar);
+ hasLocationBar_ = toolbar ? YES : locBar;
+
+ // Decide whether to hide/show based on whether there's a location bar.
+ [[self view] setHidden:!hasLocationBar_];
+
+ // Make location bar not editable when in a pop-up.
+ locationBarView_->SetEditable(toolbar);
+}
+
+- (NSView*)view {
+ if (hasToolbar_)
+ return [super view];
+ return locationBar_;
+}
+
+// (Private) Returns the backdrop to the toolbar.
+- (BackgroundGradientView*)backgroundGradientView {
+ // We really do mean |[super view]|; see our override of |-view|.
+ DCHECK([[super view] isKindOfClass:[BackgroundGradientView class]]);
+ return (BackgroundGradientView*)[super view];
+}
+
+- (id)customFieldEditorForObject:(id)obj {
+ if (obj == locationBar_) {
+ // Lazilly construct Field editor, Cocoa UI code always runs on the
+ // same thread, so there shoudn't be a race condition here.
+ if (autocompleteTextFieldEditor_.get() == nil) {
+ autocompleteTextFieldEditor_.reset(
+ [[AutocompleteTextFieldEditor alloc] init]);
+ }
+
+ // This needs to be called every time, otherwise notifications
+ // aren't sent correctly.
+ DCHECK(autocompleteTextFieldEditor_.get());
+ [autocompleteTextFieldEditor_.get() setFieldEditor:YES];
+ return autocompleteTextFieldEditor_.get();
+ }
+ return nil;
+}
+
+// Returns an array of views in the order of the outlets above.
+- (NSArray*)toolbarViews {
+ return [NSArray arrayWithObjects:backButton_, forwardButton_, reloadButton_,
+ homeButton_, wrenchButton_, locationBar_,
+ browserActionsContainerView_, nil];
+}
+
+// Moves |rect| to the right by |delta|, keeping the right side fixed by
+// shrinking the width to compensate. Passing a negative value for |deltaX|
+// moves to the left and increases the width.
+- (NSRect)adjustRect:(NSRect)rect byAmount:(CGFloat)deltaX {
+ NSRect frame = NSOffsetRect(rect, deltaX, 0);
+ frame.size.width -= deltaX;
+ return frame;
+}
+
+// Show or hide the home button based on the pref.
+- (void)showOptionalHomeButton {
+ // Ignore this message if only showing the URL bar.
+ if (!hasToolbar_)
+ return;
+ BOOL hide = showHomeButton_.GetValue() ? NO : YES;
+ if (hide == [homeButton_ isHidden])
+ return; // Nothing to do, view state matches pref state.
+
+ // Always shift the text field by the width of the home button minus one pixel
+ // since the frame edges of each button are right on top of each other. When
+ // hiding the button, reverse the direction of the movement (to the left).
+ CGFloat moveX = [homeButton_ frame].size.width - 1.0;
+ if (hide)
+ moveX *= -1; // Reverse the direction of the move.
+
+ [locationBar_ setFrame:[self adjustRect:[locationBar_ frame]
+ byAmount:moveX]];
+ [homeButton_ setHidden:hide];
+}
+
+// Install the menu wrench buttons. Calling this repeatedly is inexpensive so it
+// can be done every time the buttons are shown.
+- (void)installWrenchMenu {
+ if (wrenchMenuModel_.get())
+ return;
+ acceleratorDelegate_.reset(
+ new ToolbarControllerInternal::WrenchAcceleratorDelegate());
+
+ wrenchMenuModel_.reset(new WrenchMenuModel(
+ acceleratorDelegate_.get(), browser_));
+ [wrenchMenuController_ setModel:wrenchMenuModel_.get()];
+ [wrenchMenuController_ setUseWithPopUpButtonCell:YES];
+ [wrenchButton_ setAttachedMenu:[wrenchMenuController_ menu]];
+}
+
+- (WrenchMenuController*)wrenchMenuController {
+ return wrenchMenuController_;
+}
+
+- (void)badgeWrenchMenu {
+ // In the Windows version, the ball doesn't actually pulsate, and is always
+ // drawn with the inactive image. Why? (We follow suit, though not on the
+ // weird positioning they do that overlaps the button border.)
+ NSImage* badge = nsimage_cache::ImageNamed(@"upgrade_dot.pdf");
+ NSImage* wrenchImage = nsimage_cache::ImageNamed(kWrenchButtonImageName);
+ NSSize wrenchImageSize = [wrenchImage size];
+
+ scoped_nsobject<NSImage> overlayImage(
+ [[NSImage alloc] initWithSize:wrenchImageSize]);
+
+ [overlayImage lockFocus];
+ [badge drawAtPoint:NSZeroPoint
+ fromRect:NSZeroRect
+ operation:NSCompositeSourceOver
+ fraction:1.0];
+ [overlayImage unlockFocus];
+
+ [[wrenchButton_ cell] setOverlayImage:overlayImage];
+}
+
+- (void)prefChanged:(std::string*)prefName {
+ if (!prefName) return;
+ if (*prefName == prefs::kShowHomeButton) {
+ [self showOptionalHomeButton];
+ }
+}
+
+- (void)createBrowserActionButtons {
+ if (!browserActionsController_.get()) {
+ browserActionsController_.reset([[BrowserActionsController alloc]
+ initWithBrowser:browser_
+ containerView:browserActionsContainerView_]);
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(browserActionsContainerDragged:)
+ name:kBrowserActionGrippyDraggingNotification
+ object:browserActionsController_];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(browserActionsContainerDragFinished:)
+ name:kBrowserActionGrippyDragFinishedNotification
+ object:browserActionsController_];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(browserActionsVisibilityChanged:)
+ name:kBrowserActionVisibilityChangedNotification
+ object:browserActionsController_];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(adjustBrowserActionsContainerForNewWindow:)
+ name:NSWindowDidBecomeKeyNotification
+ object:[[self view] window]];
+ }
+ CGFloat containerWidth = [browserActionsContainerView_ isHidden] ? 0.0 :
+ NSWidth([browserActionsContainerView_ frame]);
+ if (containerWidth > 0.0)
+ [self adjustLocationSizeBy:(containerWidth * -1) animate:NO];
+}
+
+- (void)adjustBrowserActionsContainerForNewWindow:
+ (NSNotification*)notification {
+ [self toolbarFrameChanged];
+ [[NSNotificationCenter defaultCenter]
+ removeObserver:self
+ name:NSWindowDidBecomeKeyNotification
+ object:[[self view] window]];
+}
+
+- (void)browserActionsContainerDragged:(NSNotification*)notification {
+ CGFloat locationBarWidth = NSWidth([locationBar_ frame]);
+ locationBarAtMinSize_ = locationBarWidth <= kMinimumLocationBarWidth;
+ [browserActionsContainerView_ setCanDragLeft:!locationBarAtMinSize_];
+ [browserActionsContainerView_ setGrippyPinned:locationBarAtMinSize_];
+ [self adjustLocationSizeBy:
+ [browserActionsContainerView_ resizeDeltaX] animate:NO];
+}
+
+- (void)browserActionsContainerDragFinished:(NSNotification*)notification {
+ [browserActionsController_ resizeContainerAndAnimate:YES];
+ [self pinLocationBarToLeftOfBrowserActionsContainerAndAnimate:YES];
+}
+
+- (void)browserActionsVisibilityChanged:(NSNotification*)notification {
+ [self pinLocationBarToLeftOfBrowserActionsContainerAndAnimate:NO];
+}
+
+- (void)pinLocationBarToLeftOfBrowserActionsContainerAndAnimate:(BOOL)animate {
+ CGFloat locationBarXPos = NSMaxX([locationBar_ frame]);
+ CGFloat leftDistance;
+
+ if ([browserActionsContainerView_ isHidden]) {
+ CGFloat edgeXPos = [wrenchButton_ frame].origin.x;
+ leftDistance = edgeXPos - locationBarXPos - kWrenchMenuLeftPadding;
+ } else {
+ NSRect containerFrame = animate ?
+ [browserActionsContainerView_ animationEndFrame] :
+ [browserActionsContainerView_ frame];
+
+ leftDistance = containerFrame.origin.x - locationBarXPos;
+ }
+ if (leftDistance != 0.0)
+ [self adjustLocationSizeBy:leftDistance animate:animate];
+}
+
+- (void)maintainMinimumLocationBarWidth {
+ CGFloat locationBarWidth = NSWidth([locationBar_ frame]);
+ locationBarAtMinSize_ = locationBarWidth <= kMinimumLocationBarWidth;
+ if (locationBarAtMinSize_) {
+ CGFloat dX = kMinimumLocationBarWidth - locationBarWidth;
+ [self adjustLocationSizeBy:dX animate:NO];
+ }
+}
+
+- (void)toolbarFrameChanged {
+ // Do nothing if the frame changes but no Browser Action Controller is
+ // present.
+ if (!browserActionsController_.get())
+ return;
+
+ [self maintainMinimumLocationBarWidth];
+
+ if (locationBarAtMinSize_) {
+ // Once the grippy is pinned, leave it until it is explicity un-pinned.
+ [browserActionsContainerView_ setGrippyPinned:YES];
+ NSRect containerFrame = [browserActionsContainerView_ frame];
+ // Determine how much the container needs to move in case it's overlapping
+ // with the location bar.
+ CGFloat dX = NSMaxX([locationBar_ frame]) - containerFrame.origin.x;
+ containerFrame = NSOffsetRect(containerFrame, dX, 0);
+ containerFrame.size.width -= dX;
+ [browserActionsContainerView_ setFrame:containerFrame];
+ } else if (!locationBarAtMinSize_ &&
+ [browserActionsContainerView_ grippyPinned]) {
+ // Expand out the container until it hits the saved size, then unpin the
+ // grippy.
+ // Add 0.1 pixel so that it doesn't hit the minimum width codepath above.
+ CGFloat dX = NSWidth([locationBar_ frame]) -
+ (kMinimumLocationBarWidth + 0.1);
+ NSRect containerFrame = [browserActionsContainerView_ frame];
+ containerFrame = NSOffsetRect(containerFrame, -dX, 0);
+ containerFrame.size.width += dX;
+ CGFloat savedContainerWidth = [browserActionsController_ savedWidth];
+ if (NSWidth(containerFrame) >= savedContainerWidth) {
+ containerFrame = NSOffsetRect(containerFrame,
+ NSWidth(containerFrame) - savedContainerWidth, 0);
+ containerFrame.size.width = savedContainerWidth;
+ [browserActionsContainerView_ setGrippyPinned:NO];
+ }
+ [browserActionsContainerView_ setFrame:containerFrame];
+ [self pinLocationBarToLeftOfBrowserActionsContainerAndAnimate:NO];
+ }
+}
+
+- (void)adjustLocationSizeBy:(CGFloat)dX animate:(BOOL)animate {
+ // Ensure that the location bar is in its proper place.
+ NSRect locationFrame = [locationBar_ frame];
+ locationFrame.size.width += dX;
+
+ if (!animate) {
+ [locationBar_ setFrame:locationFrame];
+ return;
+ }
+
+ [NSAnimationContext beginGrouping];
+ [[NSAnimationContext currentContext] setDuration:kAnimationDuration];
+ [[locationBar_ animator] setFrame:locationFrame];
+ [NSAnimationContext endGrouping];
+}
+
+- (NSPoint)bookmarkBubblePoint {
+ return locationBarView_->GetBookmarkBubblePoint();
+}
+
+- (CGFloat)desiredHeightForCompression:(CGFloat)compressByHeight {
+ // With no toolbar, just ignore the compression.
+ return hasToolbar_ ? kBaseToolbarHeight - compressByHeight :
+ NSHeight([locationBar_ frame]);
+}
+
+- (void)setDividerOpacity:(CGFloat)opacity {
+ BackgroundGradientView* view = [self backgroundGradientView];
+ [view setShowsDivider:(opacity > 0 ? YES : NO)];
+
+ // We may not have a toolbar view (e.g., popup windows only have a location
+ // bar).
+ if ([view isKindOfClass:[ToolbarView class]]) {
+ ToolbarView* toolbarView = (ToolbarView*)view;
+ [toolbarView setDividerOpacity:opacity];
+ }
+}
+
+- (BrowserActionsController*)browserActionsController {
+ return browserActionsController_.get();
+}
+
+// (URLDropTargetController protocol)
+- (void)dropURLs:(NSArray*)urls inView:(NSView*)view at:(NSPoint)point {
+ // TODO(viettrungluu): This code is more or less copied from the code in
+ // |TabStripController|. I'll refactor this soon to make it common and expand
+ // its capabilities (e.g., allow text DnD).
+ if ([urls count] < 1) {
+ NOTREACHED();
+ return;
+ }
+
+ // TODO(viettrungluu): dropping multiple URLs?
+ if ([urls count] > 1)
+ NOTIMPLEMENTED();
+
+ // Get the first URL and fix it up.
+ GURL url(URLFixerUpper::FixupURL(
+ base::SysNSStringToUTF8([urls objectAtIndex:0]), std::string()));
+
+ browser_->GetSelectedTabContents()->OpenURL(url, GURL(), CURRENT_TAB,
+ PageTransition::TYPED);
+}
+
+// (URLDropTargetController protocol)
+- (void)indicateDropURLsInView:(NSView*)view at:(NSPoint)point {
+ // Do nothing.
+}
+
+// (URLDropTargetController protocol)
+- (void)hideDropURLsIndicatorInView:(NSView*)view {
+ // Do nothing.
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/toolbar_controller_unittest.mm b/chrome/browser/ui/cocoa/toolbar_controller_unittest.mm
new file mode 100644
index 0000000..b57fdf8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/toolbar_controller_unittest.mm
@@ -0,0 +1,237 @@
+// 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 "base/scoped_nsobject.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/gradient_button_cell.h"
+#import "chrome/browser/ui/cocoa/toolbar_controller.h"
+#import "chrome/browser/ui/cocoa/view_resizer_pong.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/common/pref_names.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+// An NSView that fakes out hitTest:.
+@interface HitView : NSView {
+ id hitTestReturn_;
+}
+@end
+
+@implementation HitView
+
+- (void)setHitTestReturn:(id)rtn {
+ hitTestReturn_ = rtn;
+}
+
+- (NSView *)hitTest:(NSPoint)aPoint {
+ return hitTestReturn_;
+}
+
+@end
+
+
+namespace {
+
+class ToolbarControllerTest : public CocoaTest {
+ public:
+
+ // Indexes that match the ordering returned by the private ToolbarController
+ // |-toolbarViews| method.
+ enum {
+ kBackIndex, kForwardIndex, kReloadIndex, kHomeIndex,
+ kWrenchIndex, kLocationIndex, kBrowserActionContainerViewIndex
+ };
+
+ ToolbarControllerTest() {
+ Browser* browser = helper_.browser();
+ CommandUpdater* updater = browser->command_updater();
+ // The default state for the commands is true, set a couple to false to
+ // ensure they get picked up correct on initialization
+ updater->UpdateCommandEnabled(IDC_BACK, false);
+ updater->UpdateCommandEnabled(IDC_FORWARD, false);
+ resizeDelegate_.reset([[ViewResizerPong alloc] init]);
+ bar_.reset(
+ [[ToolbarController alloc] initWithModel:browser->toolbar_model()
+ commands:browser->command_updater()
+ profile:helper_.profile()
+ browser:browser
+ resizeDelegate:resizeDelegate_.get()]);
+ EXPECT_TRUE([bar_ view]);
+ NSView* parent = [test_window() contentView];
+ [parent addSubview:[bar_ view]];
+ }
+
+ // Make sure the enabled state of the view is the same as the corresponding
+ // command in the updater. The views are in the declaration order of outlets.
+ void CompareState(CommandUpdater* updater, NSArray* views) {
+ EXPECT_EQ(updater->IsCommandEnabled(IDC_BACK),
+ [[views objectAtIndex:kBackIndex] isEnabled] ? true : false);
+ EXPECT_EQ(updater->IsCommandEnabled(IDC_FORWARD),
+ [[views objectAtIndex:kForwardIndex] isEnabled] ? true : false);
+ EXPECT_EQ(updater->IsCommandEnabled(IDC_RELOAD),
+ [[views objectAtIndex:kReloadIndex] isEnabled] ? true : false);
+ EXPECT_EQ(updater->IsCommandEnabled(IDC_HOME),
+ [[views objectAtIndex:kHomeIndex] isEnabled] ? true : false);
+ }
+
+ BrowserTestHelper helper_;
+ scoped_nsobject<ViewResizerPong> resizeDelegate_;
+ scoped_nsobject<ToolbarController> bar_;
+};
+
+TEST_VIEW(ToolbarControllerTest, [bar_ view])
+
+// Test the initial state that everything is sync'd up
+TEST_F(ToolbarControllerTest, InitialState) {
+ CommandUpdater* updater = helper_.browser()->command_updater();
+ CompareState(updater, [bar_ toolbarViews]);
+}
+
+// Make sure a "titlebar only" toolbar with location bar works.
+TEST_F(ToolbarControllerTest, TitlebarOnly) {
+ NSView* view = [bar_ view];
+
+ [bar_ setHasToolbar:NO hasLocationBar:YES];
+ EXPECT_NE(view, [bar_ view]);
+
+ // Simulate a popup going fullscreen and back by performing the reparenting
+ // that happens during fullscreen transitions
+ NSView* superview = [view superview];
+ [view removeFromSuperview];
+ [superview addSubview:view];
+
+ [bar_ setHasToolbar:YES hasLocationBar:YES];
+ EXPECT_EQ(view, [bar_ view]);
+
+ // Leave it off to make sure that's fine
+ [bar_ setHasToolbar:NO hasLocationBar:YES];
+}
+
+// Make sure it works in the completely undecorated case.
+TEST_F(ToolbarControllerTest, NoLocationBar) {
+ NSView* view = [bar_ view];
+
+ [bar_ setHasToolbar:NO hasLocationBar:NO];
+ EXPECT_NE(view, [bar_ view]);
+ EXPECT_TRUE([[bar_ view] isHidden]);
+
+ // Simulate a popup going fullscreen and back by performing the reparenting
+ // that happens during fullscreen transitions
+ NSView* superview = [view superview];
+ [view removeFromSuperview];
+ [superview addSubview:view];
+}
+
+// Make some changes to the enabled state of a few of the buttons and ensure
+// that we're still in sync.
+TEST_F(ToolbarControllerTest, UpdateEnabledState) {
+ CommandUpdater* updater = helper_.browser()->command_updater();
+ EXPECT_FALSE(updater->IsCommandEnabled(IDC_BACK));
+ EXPECT_FALSE(updater->IsCommandEnabled(IDC_FORWARD));
+ updater->UpdateCommandEnabled(IDC_BACK, true);
+ updater->UpdateCommandEnabled(IDC_FORWARD, true);
+ CompareState(updater, [bar_ toolbarViews]);
+}
+
+// Focus the location bar and make sure that it's the first responder.
+TEST_F(ToolbarControllerTest, FocusLocation) {
+ NSWindow* window = test_window();
+ [window makeFirstResponder:[window contentView]];
+ EXPECT_EQ([window firstResponder], [window contentView]);
+ [bar_ focusLocationBar:YES];
+ EXPECT_NE([window firstResponder], [window contentView]);
+ NSView* locationBar = [[bar_ toolbarViews] objectAtIndex:kLocationIndex];
+ EXPECT_EQ([window firstResponder], [(id)locationBar currentEditor]);
+}
+
+TEST_F(ToolbarControllerTest, LoadingState) {
+ // In its initial state, the reload button has a tag of
+ // IDC_RELOAD. When loading, it should be IDC_STOP.
+ NSButton* reload = [[bar_ toolbarViews] objectAtIndex:kReloadIndex];
+ EXPECT_EQ([reload tag], IDC_RELOAD);
+ [bar_ setIsLoading:YES force:YES];
+ EXPECT_EQ([reload tag], IDC_STOP);
+ [bar_ setIsLoading:NO force:YES];
+ EXPECT_EQ([reload tag], IDC_RELOAD);
+}
+
+// Check that toggling the state of the home button changes the visible
+// state of the home button and moves the other items accordingly.
+TEST_F(ToolbarControllerTest, ToggleHome) {
+ PrefService* prefs = helper_.profile()->GetPrefs();
+ bool showHome = prefs->GetBoolean(prefs::kShowHomeButton);
+ NSView* homeButton = [[bar_ toolbarViews] objectAtIndex:kHomeIndex];
+ EXPECT_EQ(showHome, ![homeButton isHidden]);
+
+ NSView* locationBar = [[bar_ toolbarViews] objectAtIndex:kLocationIndex];
+ NSRect originalLocationBarFrame = [locationBar frame];
+
+ // Toggle the pref and make sure the button changed state and the other
+ // views moved.
+ prefs->SetBoolean(prefs::kShowHomeButton, !showHome);
+ EXPECT_EQ(showHome, [homeButton isHidden]);
+ EXPECT_NE(NSMinX(originalLocationBarFrame), NSMinX([locationBar frame]));
+ EXPECT_NE(NSWidth(originalLocationBarFrame), NSWidth([locationBar frame]));
+}
+
+// Ensure that we don't toggle the buttons when we have a strip marked as not
+// having the full toolbar. Also ensure that the location bar doesn't change
+// size.
+TEST_F(ToolbarControllerTest, DontToggleWhenNoToolbar) {
+ [bar_ setHasToolbar:NO hasLocationBar:YES];
+ NSView* homeButton = [[bar_ toolbarViews] objectAtIndex:kHomeIndex];
+ NSView* locationBar = [[bar_ toolbarViews] objectAtIndex:kLocationIndex];
+ NSRect locationBarFrame = [locationBar frame];
+ EXPECT_EQ([homeButton isHidden], YES);
+ [bar_ showOptionalHomeButton];
+ EXPECT_EQ([homeButton isHidden], YES);
+ NSRect newLocationBarFrame = [locationBar frame];
+ EXPECT_TRUE(NSEqualRects(locationBarFrame, newLocationBarFrame));
+ newLocationBarFrame = [locationBar frame];
+ EXPECT_TRUE(NSEqualRects(locationBarFrame, newLocationBarFrame));
+}
+
+TEST_F(ToolbarControllerTest, BookmarkBubblePoint) {
+ const NSPoint starPoint = [bar_ bookmarkBubblePoint];
+ const NSRect barFrame =
+ [[bar_ view] convertRect:[[bar_ view] bounds] toView:nil];
+
+ // Make sure the star is completely inside the location bar.
+ EXPECT_TRUE(NSPointInRect(starPoint, barFrame));
+}
+
+TEST_F(ToolbarControllerTest, HoverButtonForEvent) {
+ scoped_nsobject<HitView> view([[HitView alloc]
+ initWithFrame:NSMakeRect(0,0,100,100)]);
+ [bar_ setView:view];
+ NSEvent* event = [NSEvent mouseEventWithType:NSMouseMoved
+ location:NSMakePoint(10,10)
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:nil
+ eventNumber:0
+ clickCount:0
+ pressure:0.0];
+
+ // NOT a match.
+ [view setHitTestReturn:bar_.get()];
+ EXPECT_FALSE([bar_ hoverButtonForEvent:event]);
+
+ // Not yet...
+ scoped_nsobject<NSButton> button([[NSButton alloc] init]);
+ [view setHitTestReturn:button];
+ EXPECT_FALSE([bar_ hoverButtonForEvent:event]);
+
+ // Now!
+ scoped_nsobject<GradientButtonCell> cell([[GradientButtonCell alloc] init]);
+ [button setCell:cell.get()];
+ EXPECT_TRUE([bar_ hoverButtonForEvent:nil]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/toolbar_view.h b/chrome/browser/ui/cocoa/toolbar_view.h
new file mode 100644
index 0000000..54f3135
--- /dev/null
+++ b/chrome/browser/ui/cocoa/toolbar_view.h
@@ -0,0 +1,26 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_TOOLBAR_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_TOOLBAR_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+#import "chrome/browser/ui/cocoa/background_gradient_view.h"
+
+// A view that handles any special rendering of the toolbar bar. At
+// this time it only draws a gradient. Future changes (e.g. themes)
+// may require new functionality here.
+
+@interface ToolbarView : BackgroundGradientView {
+ @private
+ // The opacity of the divider line (at the bottom of the toolbar); used when
+ // the detached bookmark bar is morphing to the normal bar and vice versa.
+ CGFloat dividerOpacity_;
+}
+
+@property(assign, nonatomic) CGFloat dividerOpacity;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_TOOLBAR_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/toolbar_view.mm b/chrome/browser/ui/cocoa/toolbar_view.mm
new file mode 100644
index 0000000..fb4bbdd
--- /dev/null
+++ b/chrome/browser/ui/cocoa/toolbar_view.mm
@@ -0,0 +1,47 @@
+ // Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/toolbar_view.h"
+
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+
+@implementation ToolbarView
+
+@synthesize dividerOpacity = dividerOpacity_;
+
+// Prevent mouse down events from moving the parent window around.
+- (BOOL)mouseDownCanMoveWindow {
+ return NO;
+}
+
+- (void)drawRect:(NSRect)rect {
+ // The toolbar's background pattern is phased relative to the
+ // tab strip view's background pattern.
+ NSPoint phase = [[self window] themePatternPhase];
+ [[NSGraphicsContext currentContext] setPatternPhase:phase];
+ [self drawBackground];
+}
+
+// Override of |-[BackgroundGradientView strokeColor]|; make it respect opacity.
+- (NSColor*)strokeColor {
+ return [[super strokeColor] colorWithAlphaComponent:[self dividerOpacity]];
+}
+
+- (BOOL)accessibilityIsIgnored {
+ return NO;
+}
+
+- (id)accessibilityAttributeValue:(NSString*)attribute {
+ if ([attribute isEqual:NSAccessibilityRoleAttribute])
+ return NSAccessibilityToolbarRole;
+
+ return [super accessibilityAttributeValue:attribute];
+}
+
+- (ViewID)viewID {
+ return VIEW_ID_TOOLBAR;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/toolbar_view_unittest.mm b/chrome/browser/ui/cocoa/toolbar_view_unittest.mm
new file mode 100644
index 0000000..242b618
--- /dev/null
+++ b/chrome/browser/ui/cocoa/toolbar_view_unittest.mm
@@ -0,0 +1,23 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/toolbar_view.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class ToolbarViewTest : public CocoaTest {
+};
+
+// This class only needs to do one thing: prevent mouse down events from moving
+// the parent window around.
+TEST_F(ToolbarViewTest, CanDragWindow) {
+ scoped_nsobject<ToolbarView> view([[ToolbarView alloc] init]);
+ EXPECT_FALSE([view mouseDownCanMoveWindow]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/translate/after_translate_infobar_controller.h b/chrome/browser/ui/cocoa/translate/after_translate_infobar_controller.h
new file mode 100644
index 0000000..80a5091
--- /dev/null
+++ b/chrome/browser/ui/cocoa/translate/after_translate_infobar_controller.h
@@ -0,0 +1,11 @@
+// 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/ui/cocoa/translate/translate_infobar_base.h"
+
+@interface AfterTranslateInfobarController : TranslateInfoBarControllerBase {
+ bool swappedLanugageButtons_;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/translate/after_translate_infobar_controller.mm b/chrome/browser/ui/cocoa/translate/after_translate_infobar_controller.mm
new file mode 100644
index 0000000..54ab77f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/translate/after_translate_infobar_controller.mm
@@ -0,0 +1,60 @@
+// 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 "chrome/browser/ui/cocoa/translate/after_translate_infobar_controller.h"
+#include "base/sys_string_conversions.h"
+
+using TranslateInfoBarUtilities::MoveControl;
+using TranslateInfoBarUtilities::VerifyControlOrderAndSpacing;
+
+@implementation AfterTranslateInfobarController
+
+- (void)loadLabelText {
+ std::vector<string16> strings;
+ TranslateInfoBarDelegate::GetAfterTranslateStrings(
+ &strings, &swappedLanugageButtons_);
+ DCHECK(strings.size() == 3U);
+ NSString* string1 = base::SysUTF16ToNSString(strings[0]);
+ NSString* string2 = base::SysUTF16ToNSString(strings[1]);
+ NSString* string3 = base::SysUTF16ToNSString(strings[2]);
+
+ [label1_ setStringValue:string1];
+ [label2_ setStringValue:string2];
+ [label3_ setStringValue:string3];
+}
+
+- (void)layout {
+ [self removeOkCancelButtons];
+ [optionsPopUp_ setHidden:NO];
+ NSView* firstPopup = fromLanguagePopUp_;
+ NSView* lastPopup = toLanguagePopUp_;
+ if (swappedLanugageButtons_) {
+ firstPopup = toLanguagePopUp_;
+ lastPopup = fromLanguagePopUp_;
+ }
+ NSView* lastControl = lastPopup;
+
+ MoveControl(label1_, firstPopup, spaceBetweenControls_ / 2, true);
+ MoveControl(firstPopup, label2_, spaceBetweenControls_ / 2, true);
+ MoveControl(label2_, lastPopup, spaceBetweenControls_ / 2, true);
+ MoveControl(lastPopup, label3_, 0, true);
+ lastControl = label3_;
+
+ MoveControl(lastControl, showOriginalButton_, spaceBetweenControls_ * 2,
+ true);
+}
+
+- (NSArray*)visibleControls {
+ return [NSArray arrayWithObjects:label1_.get(), fromLanguagePopUp_.get(),
+ label2_.get(), toLanguagePopUp_.get(), label3_.get(),
+ showOriginalButton_.get(), nil];
+}
+
+- (bool)verifyLayout {
+ if ([optionsPopUp_ isHidden])
+ return false;
+ return [super verifyLayout];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/translate/before_translate_infobar_controller.h b/chrome/browser/ui/cocoa/translate/before_translate_infobar_controller.h
new file mode 100644
index 0000000..a96f9af
--- /dev/null
+++ b/chrome/browser/ui/cocoa/translate/before_translate_infobar_controller.h
@@ -0,0 +1,23 @@
+// 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/ui/cocoa/translate/translate_infobar_base.h"
+
+@interface BeforeTranslateInfobarController : TranslateInfoBarControllerBase {
+ scoped_nsobject<NSButton> alwaysTranslateButton_;
+ scoped_nsobject<NSButton> neverTranslateButton_;
+}
+
+// Creates and initializes the alwaysTranslate and neverTranslate buttons.
+- (void)initializeExtraControls;
+
+@end
+
+@interface BeforeTranslateInfobarController (TestingAPI)
+
+- (NSButton*)alwaysTranslateButton;
+- (NSButton*)neverTranslateButton;
+
+@end
+
diff --git a/chrome/browser/ui/cocoa/translate/before_translate_infobar_controller.mm b/chrome/browser/ui/cocoa/translate/before_translate_infobar_controller.mm
new file mode 100644
index 0000000..0beedad
--- /dev/null
+++ b/chrome/browser/ui/cocoa/translate/before_translate_infobar_controller.mm
@@ -0,0 +1,123 @@
+// 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 "chrome/browser/ui/cocoa/translate/before_translate_infobar_controller.h"
+
+#include "app/l10n_util.h"
+#include "base/sys_string_conversions.h"
+#include "grit/generated_resources.h"
+
+using TranslateInfoBarUtilities::MoveControl;
+using TranslateInfoBarUtilities::VerifyControlOrderAndSpacing;
+
+namespace {
+
+NSButton* CreateNSButtonWithResourceIDAndParameter(
+ int resourceId, const string16& param) {
+ string16 title = l10n_util::GetStringFUTF16(resourceId, param);
+ NSButton* button = [[NSButton alloc] init];
+ [button setTitle:base::SysUTF16ToNSString(title)];
+ [button setBezelStyle:NSTexturedRoundedBezelStyle];
+ return button;
+}
+
+} // namespace
+
+@implementation BeforeTranslateInfobarController
+
+- (id) initWithDelegate:(InfoBarDelegate *)delegate {
+ if ((self = [super initWithDelegate:delegate])) {
+ [self initializeExtraControls];
+ }
+ return self;
+}
+
+- (void)initializeExtraControls {
+ TranslateInfoBarDelegate* delegate = [self delegate];
+ const string16& language = delegate->GetLanguageDisplayableNameAt(
+ delegate->original_language_index());
+ neverTranslateButton_.reset(
+ CreateNSButtonWithResourceIDAndParameter(
+ IDS_TRANSLATE_INFOBAR_NEVER_TRANSLATE, language));
+ [neverTranslateButton_ setTarget:self];
+ [neverTranslateButton_ setAction:@selector(neverTranslate:)];
+
+ alwaysTranslateButton_.reset(
+ CreateNSButtonWithResourceIDAndParameter(
+ IDS_TRANSLATE_INFOBAR_ALWAYS_TRANSLATE, language));
+ [alwaysTranslateButton_ setTarget:self];
+ [alwaysTranslateButton_ setAction:@selector(alwaysTranslate:)];
+}
+
+- (void)layout {
+ MoveControl(label1_, fromLanguagePopUp_, spaceBetweenControls_ / 2, true);
+ MoveControl(fromLanguagePopUp_, label2_, spaceBetweenControls_, true);
+ MoveControl(label2_, okButton_, spaceBetweenControls_, true);
+ MoveControl(okButton_, cancelButton_, spaceBetweenControls_, true);
+ NSView* lastControl = cancelButton_;
+ if (neverTranslateButton_.get()) {
+ MoveControl(lastControl, neverTranslateButton_.get(),
+ spaceBetweenControls_, true);
+ lastControl = neverTranslateButton_.get();
+ }
+ if (alwaysTranslateButton_.get()) {
+ MoveControl(lastControl, alwaysTranslateButton_.get(),
+ spaceBetweenControls_, true);
+ }
+}
+
+- (void)loadLabelText {
+ size_t offset = 0;
+ string16 text =
+ l10n_util::GetStringFUTF16(IDS_TRANSLATE_INFOBAR_BEFORE_MESSAGE,
+ string16(), &offset);
+ NSString* string1 = base::SysUTF16ToNSString(text.substr(0, offset));
+ NSString* string2 = base::SysUTF16ToNSString(text.substr(offset));
+ [label1_ setStringValue:string1];
+ [label2_ setStringValue:string2];
+ [label3_ setStringValue:@""];
+}
+
+- (NSArray*)visibleControls {
+ NSMutableArray* visibleControls = [NSMutableArray arrayWithObjects:
+ label1_.get(), fromLanguagePopUp_.get(), label2_.get(),
+ okButton_, cancelButton_, nil];
+
+ if ([self delegate]->ShouldShowNeverTranslateButton())
+ [visibleControls addObject:neverTranslateButton_.get()];
+
+ if ([self delegate]->ShouldShowAlwaysTranslateButton())
+ [visibleControls addObject:alwaysTranslateButton_.get()];
+
+ return visibleControls;
+}
+
+// This is called when the "Never Translate [language]" button is pressed.
+- (void)neverTranslate:(id)sender {
+ [self delegate]->NeverTranslatePageLanguage();
+}
+
+// This is called when the "Always Translate [language]" button is pressed.
+- (void)alwaysTranslate:(id)sender {
+ [self delegate]->AlwaysTranslatePageLanguage();
+}
+
+- (bool)verifyLayout {
+ if ([optionsPopUp_ isHidden])
+ return false;
+ return [super verifyLayout];
+}
+
+@end
+
+@implementation BeforeTranslateInfobarController (TestingAPI)
+
+- (NSButton*)alwaysTranslateButton {
+ return alwaysTranslateButton_.get();
+}
+- (NSButton*)neverTranslateButton {
+ return neverTranslateButton_.get();
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/translate/translate_infobar_base.h b/chrome/browser/ui/cocoa/translate/translate_infobar_base.h
new file mode 100644
index 0000000..306dad1
--- /dev/null
+++ b/chrome/browser/ui/cocoa/translate/translate_infobar_base.h
@@ -0,0 +1,163 @@
+// 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_UI_COCOA_TRANSLATE_INFOBAR_BASE_H_
+#define CHROME_BROWSER_UI_COCOA_TRANSLATE_INFOBAR_BASE_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+#import "chrome/browser/ui/cocoa/infobar_controller.h"
+
+#import "base/cocoa_protocols_mac.h"
+#import "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/translate/languages_menu_model.h"
+#include "chrome/browser/translate/options_menu_model.h"
+#include "chrome/browser/translate/translate_infobar_delegate.h"
+#include "chrome/common/translate_errors.h"
+
+class TranslateInfoBarMenuModel;
+
+#pragma mark TranslateInfoBarUtilities helper functions.
+namespace TranslateInfoBarUtilities {
+
+// Move the |toMove| view |spacing| pixels before/after the |anchor| view.
+// |after| signifies the side of |anchor| on which to place |toMove|.
+void MoveControl(NSView* anchor, NSView* toMove, int spacing, bool after);
+
+// Vertically center |toMove| in its container.
+void VerticallyCenterView(NSView *toMove);
+// Check that the control |before| is ordered visually before the |after|
+// control.
+// Also, check that there is space between them.
+bool VerifyControlOrderAndSpacing(id before, id after);
+
+// Creates a label control in the style we need for the translate infobar's
+// labels within |bounds|.
+NSTextField* CreateLabel(NSRect bounds);
+
+// Adds an item with the specified properties to |menu|.
+void AddMenuItem(NSMenu *menu, id target, SEL selector, NSString* title,
+ int tag, bool enabled, bool checked);
+
+} // namespace
+
+// The base class for the three translate infobars. This class does all of the
+// heavy UI lifting, while deferring to the subclass to tell it what views
+// should be shown and where. Subclasses need to implement:
+// - (void)layout;
+// - (void)loadLabelText;
+// - (void)visibleControls;
+// - (bool)verifyLayout; // For testing.
+@interface TranslateInfoBarControllerBase : InfoBarController<NSMenuDelegate> {
+ @protected
+ scoped_nsobject<NSTextField> label1_;
+ scoped_nsobject<NSTextField> label2_;
+ scoped_nsobject<NSTextField> label3_;
+ scoped_nsobject<NSPopUpButton> fromLanguagePopUp_;
+ scoped_nsobject<NSPopUpButton> toLanguagePopUp_;
+ scoped_nsobject<NSPopUpButton> optionsPopUp_;
+ scoped_nsobject<NSButton> showOriginalButton_;
+ // This is the button used in the translate message infobar. It can either be
+ // a "Try Again" button, or a "Show Original" button in the case that the
+ // page was translated from an unknown language.
+ scoped_nsobject<NSButton> translateMessageButton_;
+
+ // In the current locale, are the "from" and "to" language popup menu
+ // flipped from what they'd appear in English.
+ bool swappedLanguagePlaceholders_;
+
+ // Space between controls in pixels - read from the NIB.
+ CGFloat spaceBetweenControls_;
+
+ scoped_ptr<LanguagesMenuModel> originalLanguageMenuModel_;
+ scoped_ptr<LanguagesMenuModel> targetLanguageMenuModel_;
+ scoped_ptr<OptionsMenuModel> optionsMenuModel_;
+}
+
+// Returns the delegate as a TranslateInfoBarDelegate.
+- (TranslateInfoBarDelegate*)delegate;
+
+// Called when the "Show Original" button is pressed.
+- (IBAction)showOriginal:(id)sender;
+
+@end
+
+@interface TranslateInfoBarControllerBase (ProtectedAPI)
+
+// Resizes or hides the options button based on how much space is available
+// so that it doesn't overlap other buttons.
+// lastView is the rightmost view, the first one that the options button
+// would overlap with.
+- (void)adjustOptionsButtonSizeAndVisibilityForView:(NSView*)lastView;
+
+// Move all the currently visible views into the correct place for the
+// current mode.
+// Must be implemented by the subclass.
+- (void)layout;
+
+// Loads the text for the 3 labels. There is only one message, but since
+// it has controls separating parts of it, it is separated into 3 separate
+// labels.
+// Must be implemented by the subclass.
+- (void)loadLabelText;
+
+// Returns the controls that are visible in the subclasses infobar. The
+// default implementation returns an empty array. The controls should
+// be returned in the order they are displayed, otherwise the layout test
+// will fail.
+// Must be implemented by the subclass.
+- (NSArray*)visibleControls;
+
+// Shows the array of controls provided by the subclass.
+- (void)showVisibleControls:(NSArray*)visibleControls;
+
+// Hides the OK and Cancel buttons.
+- (void)removeOkCancelButtons;
+
+// Called when the source or target language selection changes in a menu.
+// |newLanguageIdx| is the index of the newly selected item in the appropriate
+// menu.
+- (void)sourceLanguageModified:(NSInteger)newLanguageIdx;
+- (void)targetLanguageModified:(NSInteger)newLanguageIdx;
+
+// Called when an item in one of the toolbar's language or options
+// menus is selected.
+- (void)languageMenuChanged:(id)item;
+- (void)optionsMenuChanged:(id)item;
+
+// Teardown and rebuild the options menu. When the infobar is small, the
+// options menu is shrunk to just a drop down arrow, so the title needs
+// to be empty.
+- (void)rebuildOptionsMenu:(BOOL)hideTitle;
+
+// Whether or not this infobar should show the options popup.
+- (BOOL)shouldShowOptionsPopUp;
+
+@end // TranslateInfoBarControllerBase (ProtectedAPI)
+
+#pragma mark TestingAPI
+
+@interface TranslateInfoBarControllerBase (TestingAPI)
+
+// All the controls used in any of the translate states.
+// This is used for verifying layout and for setting the
+// correct styles on each button.
+- (NSArray*)allControls;
+
+// Verifies that the layout of the infobar is correct.
+// Must be implmented by the subclass.
+- (bool)verifyLayout;
+
+// Returns the underlying options menu.
+- (NSMenu*)optionsMenu;
+
+// Returns |translateMessageButton_|, see declaration of member
+// variable for a full description.
+- (NSButton*)translateMessageButton;
+
+@end // TranslateInfoBarControllerBase (TestingAPI)
+
+
+#endif // CHROME_BROWSER_UI_COCOA_TRANSLATE_INFOBAR_BASE_H_
diff --git a/chrome/browser/ui/cocoa/translate/translate_infobar_base.mm b/chrome/browser/ui/cocoa/translate/translate_infobar_base.mm
new file mode 100644
index 0000000..4a08895
--- /dev/null
+++ b/chrome/browser/ui/cocoa/translate/translate_infobar_base.mm
@@ -0,0 +1,642 @@
+// 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/ui/cocoa/translate/translate_infobar_base.h"
+
+#include "app/l10n_util.h"
+#include "base/logging.h"
+#include "base/mac_util.h"
+#include "base/metrics/histogram.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/translate/translate_infobar_delegate.h"
+#import "chrome/browser/ui/cocoa/hover_close_button.h"
+#include "chrome/browser/ui/cocoa/infobar.h"
+#import "chrome/browser/ui/cocoa/infobar_controller.h"
+#import "chrome/browser/ui/cocoa/infobar_gradient_view.h"
+#include "chrome/browser/ui/cocoa/translate/after_translate_infobar_controller.h"
+#import "chrome/browser/ui/cocoa/translate/before_translate_infobar_controller.h"
+#include "chrome/browser/ui/cocoa/translate/translate_message_infobar_controller.h"
+#include "grit/generated_resources.h"
+#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+using TranslateInfoBarUtilities::MoveControl;
+using TranslateInfoBarUtilities::VerticallyCenterView;
+using TranslateInfoBarUtilities::VerifyControlOrderAndSpacing;
+using TranslateInfoBarUtilities::CreateLabel;
+using TranslateInfoBarUtilities::AddMenuItem;
+
+#pragma mark TranslateInfoBarUtilities helper functions.
+
+namespace TranslateInfoBarUtilities {
+
+// Move the |toMove| view |spacing| pixels before/after the |anchor| view.
+// |after| signifies the side of |anchor| on which to place |toMove|.
+void MoveControl(NSView* anchor, NSView* toMove, int spacing, bool after) {
+ NSRect anchorFrame = [anchor frame];
+ NSRect toMoveFrame = [toMove frame];
+
+ // At the time of this writing, OS X doesn't natively support BiDi UIs, but
+ // it doesn't hurt to be forward looking.
+ bool toRight = after;
+
+ if (toRight) {
+ toMoveFrame.origin.x = NSMaxX(anchorFrame) + spacing;
+ } else {
+ // Place toMove to theleft of anchor.
+ toMoveFrame.origin.x = NSMinX(anchorFrame) -
+ spacing - NSWidth(toMoveFrame);
+ }
+ [toMove setFrame:toMoveFrame];
+}
+
+// Check that the control |before| is ordered visually before the |after|
+// control.
+// Also, check that there is space between them.
+bool VerifyControlOrderAndSpacing(id before, id after) {
+ NSRect beforeFrame = [before frame];
+ NSRect afterFrame = [after frame];
+ return NSMinX(afterFrame) >= NSMaxX(beforeFrame);
+}
+
+// Vertically center |toMove| in its container.
+void VerticallyCenterView(NSView *toMove) {
+ NSRect superViewFrame = [[toMove superview] frame];
+ NSRect viewFrame = [toMove frame];
+ viewFrame.origin.y =
+ floor((NSHeight(superViewFrame) - NSHeight(viewFrame))/2.0);
+ [toMove setFrame:viewFrame];
+}
+
+// Creates a label control in the style we need for the translate infobar's
+// labels within |bounds|.
+NSTextField* CreateLabel(NSRect bounds) {
+ NSTextField* ret = [[NSTextField alloc] initWithFrame:bounds];
+ [ret setEditable:NO];
+ [ret setDrawsBackground:NO];
+ [ret setBordered:NO];
+ return ret;
+}
+
+// Adds an item with the specified properties to |menu|.
+void AddMenuItem(NSMenu *menu, id target, SEL selector, NSString* title,
+ int tag, bool enabled, bool checked) {
+ if (tag == -1) {
+ [menu addItem:[NSMenuItem separatorItem]];
+ } else {
+ NSMenuItem* item = [[[NSMenuItem alloc]
+ initWithTitle:title
+ action:selector
+ keyEquivalent:@""] autorelease];
+ [item setTag:tag];
+ [menu addItem:item];
+ [item setTarget:target];
+ if (checked)
+ [item setState:NSOnState];
+ if (!enabled)
+ [item setEnabled:NO];
+ }
+}
+
+} // namespace TranslateInfoBarUtilities
+
+// TranslateInfoBarDelegate views specific method:
+InfoBar* TranslateInfoBarDelegate::CreateInfoBar() {
+ TranslateInfoBarControllerBase* infobar_controller = NULL;
+ switch (type_) {
+ case BEFORE_TRANSLATE:
+ infobar_controller =
+ [[BeforeTranslateInfobarController alloc] initWithDelegate:this];
+ break;
+ case AFTER_TRANSLATE:
+ infobar_controller =
+ [[AfterTranslateInfobarController alloc] initWithDelegate:this];
+ break;
+ case TRANSLATING:
+ case TRANSLATION_ERROR:
+ infobar_controller =
+ [[TranslateMessageInfobarController alloc] initWithDelegate:this];
+ break;
+ default:
+ NOTREACHED();
+ }
+ return new InfoBar(infobar_controller);
+}
+
+@implementation TranslateInfoBarControllerBase (FrameChangeObserver)
+
+// Triggered when the frame changes. This will figure out what size and
+// visibility the options popup should be.
+- (void)didChangeFrame:(NSNotification*)notification {
+ [self adjustOptionsButtonSizeAndVisibilityForView:
+ [[self visibleControls] lastObject]];
+}
+
+@end
+
+
+@interface TranslateInfoBarControllerBase (Private)
+
+// Removes all controls so that layout can add in only the controls
+// required.
+- (void)clearAllControls;
+
+// Create all the various controls we need for the toolbar.
+- (void)constructViews;
+
+// Reloads text for all labels for the current state.
+- (void)loadLabelText:(TranslateErrors::Type)error;
+
+// Set the infobar background gradient.
+- (void)setInfoBarGradientColor;
+
+// Main function to update the toolbar graphic state and data model after
+// the state has changed.
+// Controls are moved around as needed and visibility changed to match the
+// current state.
+- (void)updateState;
+
+// Called when the source or target language selection changes in a menu.
+// |newLanguageIdx| is the index of the newly selected item in the appropriate
+// menu.
+- (void)sourceLanguageModified:(NSInteger)newLanguageIdx;
+- (void)targetLanguageModified:(NSInteger)newLanguageIdx;
+
+// Completely rebuild "from" and "to" language menus from the data model.
+- (void)populateLanguageMenus;
+
+@end
+
+#pragma mark TranslateInfoBarController class
+
+@implementation TranslateInfoBarControllerBase
+
+- (id)initWithDelegate:(InfoBarDelegate*)delegate {
+ if ((self = [super initWithDelegate:delegate])) {
+ originalLanguageMenuModel_.reset(
+ new LanguagesMenuModel([self delegate],
+ LanguagesMenuModel::ORIGINAL));
+
+ targetLanguageMenuModel_.reset(
+ new LanguagesMenuModel([self delegate],
+ LanguagesMenuModel::TARGET));
+ }
+ return self;
+}
+
+- (TranslateInfoBarDelegate*)delegate {
+ return reinterpret_cast<TranslateInfoBarDelegate*>(delegate_);
+}
+
+- (void)constructViews {
+ // Using a zero or very large frame causes GTMUILocalizerAndLayoutTweaker
+ // to not resize the view properly so we take the bounds of the first label
+ // which is contained in the nib.
+ NSRect bogusFrame = [label_ frame];
+ label1_.reset(CreateLabel(bogusFrame));
+ label2_.reset(CreateLabel(bogusFrame));
+ label3_.reset(CreateLabel(bogusFrame));
+
+ optionsPopUp_.reset([[NSPopUpButton alloc] initWithFrame:bogusFrame
+ pullsDown:YES]);
+ fromLanguagePopUp_.reset([[NSPopUpButton alloc] initWithFrame:bogusFrame
+ pullsDown:NO]);
+ toLanguagePopUp_.reset([[NSPopUpButton alloc] initWithFrame:bogusFrame
+ pullsDown:NO]);
+ showOriginalButton_.reset([[NSButton alloc] init]);
+ translateMessageButton_.reset([[NSButton alloc] init]);
+}
+
+- (void)sourceLanguageModified:(NSInteger)newLanguageIdx {
+ DCHECK_GT(newLanguageIdx, -1);
+ if (newLanguageIdx == [self delegate]->original_language_index())
+ return;
+ [self delegate]->SetOriginalLanguage(newLanguageIdx);
+ int commandId = IDC_TRANSLATE_ORIGINAL_LANGUAGE_BASE + newLanguageIdx;
+ int newMenuIdx = [fromLanguagePopUp_ indexOfItemWithTag:commandId];
+ [fromLanguagePopUp_ selectItemAtIndex:newMenuIdx];
+}
+
+- (void)targetLanguageModified:(NSInteger)newLanguageIdx {
+ DCHECK_GT(newLanguageIdx, -1);
+ if (newLanguageIdx == [self delegate]->target_language_index())
+ return;
+ [self delegate]->SetTargetLanguage(newLanguageIdx);
+ int commandId = IDC_TRANSLATE_TARGET_LANGUAGE_BASE + newLanguageIdx;
+ int newMenuIdx = [toLanguagePopUp_ indexOfItemWithTag:commandId];
+ [toLanguagePopUp_ selectItemAtIndex:newMenuIdx];
+}
+
+- (void)loadLabelText {
+ // Do nothing by default, should be implemented by subclasses.
+}
+
+- (void)updateState {
+ [self loadLabelText];
+ [self clearAllControls];
+ [self showVisibleControls:[self visibleControls]];
+ [optionsPopUp_ setHidden:![self shouldShowOptionsPopUp]];
+ [self layout];
+ [self adjustOptionsButtonSizeAndVisibilityForView:
+ [[self visibleControls] lastObject]];
+}
+
+- (void)setInfoBarGradientColor {
+ NSColor* startingColor = [NSColor colorWithCalibratedWhite:0.93 alpha:1.0];
+ NSColor* endingColor = [NSColor colorWithCalibratedWhite:0.85 alpha:1.0];
+ NSGradient* translateInfoBarGradient =
+ [[[NSGradient alloc] initWithStartingColor:startingColor
+ endingColor:endingColor] autorelease];
+
+ [infoBarView_ setGradient:translateInfoBarGradient];
+ [infoBarView_
+ setStrokeColor:[NSColor colorWithCalibratedWhite:0.75 alpha:1.0]];
+}
+
+- (void)removeOkCancelButtons {
+ // Removing okButton_ & cancelButton_ from the view may cause them
+ // to be released and since we can still access them from other areas
+ // in the code later, we need them to be nil when this happens.
+ [okButton_ removeFromSuperview];
+ okButton_ = nil;
+ [cancelButton_ removeFromSuperview];
+ cancelButton_ = nil;
+}
+
+- (void)clearAllControls {
+ // Step 1: remove all controls from the infobar so we have a clean slate.
+ NSArray *allControls = [self allControls];
+
+ for (NSControl* control in allControls) {
+ if ([control superview])
+ [control removeFromSuperview];
+ }
+}
+
+- (void)showVisibleControls:(NSArray*)visibleControls {
+ NSRect optionsFrame = [optionsPopUp_ frame];
+ for (NSControl* control in visibleControls) {
+ [GTMUILocalizerAndLayoutTweaker sizeToFitView:control];
+ [control setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin |
+ NSViewMaxYMargin];
+
+ // Need to check if a view is already attached since |label1_| is always
+ // parented and we don't want to add it again.
+ if (![control superview])
+ [infoBarView_ addSubview:control];
+
+ if ([control isKindOfClass:[NSButton class]])
+ VerticallyCenterView(control);
+
+ // Make "from" and "to" language popup menus the same size as the options
+ // menu.
+ // We don't autosize since some languages names are really long causing
+ // the toolbar to overflow.
+ if ([control isKindOfClass:[NSPopUpButton class]])
+ [control setFrame:optionsFrame];
+ }
+}
+
+- (void)layout {
+
+}
+
+- (NSArray*)visibleControls {
+ return [NSArray array];
+}
+
+- (void)rebuildOptionsMenu:(BOOL)hideTitle {
+ if (![self shouldShowOptionsPopUp])
+ return;
+
+ // The options model doesn't know how to handle state transitions, so rebuild
+ // it each time through here.
+ optionsMenuModel_.reset(
+ new OptionsMenuModel([self delegate]));
+
+ [optionsPopUp_ removeAllItems];
+ // Set title.
+ NSString* optionsLabel = hideTitle ? @"" :
+ l10n_util::GetNSString(IDS_TRANSLATE_INFOBAR_OPTIONS);
+ [optionsPopUp_ addItemWithTitle:optionsLabel];
+
+ // Populate options menu.
+ NSMenu* optionsMenu = [optionsPopUp_ menu];
+ [optionsMenu setAutoenablesItems:NO];
+ for (int i = 0; i < optionsMenuModel_->GetItemCount(); ++i) {
+ NSString* title = base::SysUTF16ToNSString(
+ optionsMenuModel_->GetLabelAt(i));
+ int cmd = optionsMenuModel_->GetCommandIdAt(i);
+ bool checked = optionsMenuModel_->IsItemCheckedAt(i);
+ bool enabled = optionsMenuModel_->IsEnabledAt(i);
+ AddMenuItem(optionsMenu,
+ self,
+ @selector(optionsMenuChanged:),
+ title,
+ cmd,
+ enabled,
+ checked);
+ }
+}
+
+- (BOOL)shouldShowOptionsPopUp {
+ return YES;
+}
+
+- (void)populateLanguageMenus {
+ NSMenu* originalLanguageMenu = [fromLanguagePopUp_ menu];
+ [originalLanguageMenu setAutoenablesItems:NO];
+ int selectedMenuIndex = 0;
+ int selectedLangIndex = [self delegate]->original_language_index();
+ for (int i = 0; i < originalLanguageMenuModel_->GetItemCount(); ++i) {
+ NSString* title = base::SysUTF16ToNSString(
+ originalLanguageMenuModel_->GetLabelAt(i));
+ int cmd = originalLanguageMenuModel_->GetCommandIdAt(i);
+ bool checked = (cmd == selectedLangIndex);
+ if (checked)
+ selectedMenuIndex = i;
+ bool enabled = originalLanguageMenuModel_->IsEnabledAt(i);
+ cmd += IDC_TRANSLATE_ORIGINAL_LANGUAGE_BASE;
+ AddMenuItem(originalLanguageMenu,
+ self,
+ @selector(languageMenuChanged:),
+ title,
+ cmd,
+ enabled,
+ checked);
+ }
+ [fromLanguagePopUp_ selectItemAtIndex:selectedMenuIndex];
+
+ NSMenu* targetLanguageMenu = [toLanguagePopUp_ menu];
+ [targetLanguageMenu setAutoenablesItems:NO];
+ selectedLangIndex = [self delegate]->target_language_index();
+ for (int i = 0; i < targetLanguageMenuModel_->GetItemCount(); ++i) {
+ NSString* title = base::SysUTF16ToNSString(
+ targetLanguageMenuModel_->GetLabelAt(i));
+ int cmd = targetLanguageMenuModel_->GetCommandIdAt(i);
+ bool checked = (cmd == selectedLangIndex);
+ if (checked)
+ selectedMenuIndex = i;
+ bool enabled = targetLanguageMenuModel_->IsEnabledAt(i);
+ cmd += IDC_TRANSLATE_TARGET_LANGUAGE_BASE;
+ AddMenuItem(targetLanguageMenu,
+ self,
+ @selector(languageMenuChanged:),
+ title,
+ cmd,
+ enabled,
+ checked);
+ }
+ [toLanguagePopUp_ selectItemAtIndex:selectedMenuIndex];
+}
+
+- (void)addAdditionalControls {
+ using l10n_util::GetNSString;
+ using l10n_util::GetNSStringWithFixup;
+
+ // Get layout information from the NIB.
+ NSRect okButtonFrame = [okButton_ frame];
+ NSRect cancelButtonFrame = [cancelButton_ frame];
+ spaceBetweenControls_ = NSMinX(cancelButtonFrame) - NSMaxX(okButtonFrame);
+
+ // Set infobar background color.
+ [self setInfoBarGradientColor];
+
+ // Instantiate additional controls.
+ [self constructViews];
+
+ // Set ourselves as the delegate for the options menu so we can populate it
+ // dynamically.
+ [[optionsPopUp_ menu] setDelegate:self];
+
+ // Replace label_ with label1_ so we get a consistent look between all the
+ // labels we display in the translate view.
+ [[label_ superview] replaceSubview:label_ with:label1_.get()];
+ label_.reset(); // Now released.
+
+ // Populate contextual menus.
+ [self rebuildOptionsMenu:NO];
+ [self populateLanguageMenus];
+
+ // Set OK & Cancel text.
+ [okButton_ setTitle:GetNSStringWithFixup(IDS_TRANSLATE_INFOBAR_ACCEPT)];
+ [cancelButton_ setTitle:GetNSStringWithFixup(IDS_TRANSLATE_INFOBAR_DENY)];
+
+ // Set up "Show original" and "Try again" buttons.
+ [showOriginalButton_ setFrame:okButtonFrame];
+
+ // Set each of the buttons and popups to the NSTexturedRoundedBezelStyle
+ // (metal-looking) style.
+ NSArray* allControls = [self allControls];
+ for (NSControl* control in allControls) {
+ if (![control isKindOfClass:[NSButton class]])
+ continue;
+ NSButton* button = (NSButton*)control;
+ [button setBezelStyle:NSTexturedRoundedBezelStyle];
+ if ([button isKindOfClass:[NSPopUpButton class]]) {
+ [[button cell] setArrowPosition:NSPopUpArrowAtBottom];
+ }
+ }
+ // The options button is handled differently than the rest as it floats
+ // to the right.
+ [optionsPopUp_ setBezelStyle:NSTexturedRoundedBezelStyle];
+ [[optionsPopUp_ cell] setArrowPosition:NSPopUpArrowAtBottom];
+
+ [showOriginalButton_ setTarget:self];
+ [showOriginalButton_ setAction:@selector(showOriginal:)];
+ [translateMessageButton_ setTarget:self];
+ [translateMessageButton_ setAction:@selector(messageButtonPressed:)];
+
+ [showOriginalButton_
+ setTitle:GetNSStringWithFixup(IDS_TRANSLATE_INFOBAR_REVERT)];
+
+ // Add and configure controls that are visible in all modes.
+ [optionsPopUp_ setAutoresizingMask:NSViewMinXMargin | NSViewMinYMargin |
+ NSViewMaxYMargin];
+ // Add "options" popup z-ordered below all other controls so when we
+ // resize the toolbar it doesn't hide them.
+ [infoBarView_ addSubview:optionsPopUp_
+ positioned:NSWindowBelow
+ relativeTo:nil];
+ [GTMUILocalizerAndLayoutTweaker sizeToFitView:optionsPopUp_];
+ MoveControl(closeButton_, optionsPopUp_, spaceBetweenControls_, false);
+ VerticallyCenterView(optionsPopUp_);
+
+ [infoBarView_ setPostsFrameChangedNotifications:YES];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(didChangeFrame:)
+ name:NSViewFrameDidChangeNotification
+ object:infoBarView_];
+ // Show and place GUI elements.
+ [self updateState];
+}
+
+- (void)adjustOptionsButtonSizeAndVisibilityForView:(NSView*)lastView {
+ [optionsPopUp_ setHidden:NO];
+ [self rebuildOptionsMenu:NO];
+ [[optionsPopUp_ cell] setArrowPosition:NSPopUpArrowAtBottom];
+ [optionsPopUp_ sizeToFit];
+
+ MoveControl(closeButton_, optionsPopUp_, spaceBetweenControls_, false);
+ if (!VerifyControlOrderAndSpacing(lastView, optionsPopUp_)) {
+ [self rebuildOptionsMenu:YES];
+ NSRect oldFrame = [optionsPopUp_ frame];
+ oldFrame.size.width = NSHeight(oldFrame);
+ [optionsPopUp_ setFrame:oldFrame];
+ [[optionsPopUp_ cell] setArrowPosition:NSPopUpArrowAtCenter];
+ MoveControl(closeButton_, optionsPopUp_, spaceBetweenControls_, false);
+ if (!VerifyControlOrderAndSpacing(lastView, optionsPopUp_)) {
+ [optionsPopUp_ setHidden:YES];
+ }
+ }
+}
+
+// Called when "Translate" button is clicked.
+- (IBAction)ok:(id)sender {
+ TranslateInfoBarDelegate* delegate = [self delegate];
+ TranslateInfoBarDelegate::Type state = delegate->type();
+ DCHECK(state == TranslateInfoBarDelegate::BEFORE_TRANSLATE ||
+ state == TranslateInfoBarDelegate::TRANSLATION_ERROR);
+ delegate->Translate();
+ UMA_HISTOGRAM_COUNTS("Translate.Translate", 1);
+}
+
+// Called when someone clicks on the "Nope" button.
+- (IBAction)cancel:(id)sender {
+ DCHECK(
+ [self delegate]->type() == TranslateInfoBarDelegate::BEFORE_TRANSLATE);
+ [self delegate]->TranslationDeclined();
+ UMA_HISTOGRAM_COUNTS("Translate.DeclineTranslate", 1);
+ [super dismiss:nil];
+}
+
+- (void)messageButtonPressed:(id)sender {
+ [self delegate]->MessageInfoBarButtonPressed();
+}
+
+- (IBAction)showOriginal:(id)sender {
+ [self delegate]->RevertTranslation();
+}
+
+// Called when any of the language drop down menus are changed.
+- (void)languageMenuChanged:(id)item {
+ if ([item respondsToSelector:@selector(tag)]) {
+ int cmd = [item tag];
+ if (cmd >= IDC_TRANSLATE_TARGET_LANGUAGE_BASE) {
+ cmd -= IDC_TRANSLATE_TARGET_LANGUAGE_BASE;
+ [self targetLanguageModified:cmd];
+ return;
+ } else if (cmd >= IDC_TRANSLATE_ORIGINAL_LANGUAGE_BASE) {
+ cmd -= IDC_TRANSLATE_ORIGINAL_LANGUAGE_BASE;
+ [self sourceLanguageModified:cmd];
+ return;
+ }
+ }
+ NOTREACHED() << "Language menu was changed with a bad language ID";
+}
+
+// Called when the options menu is changed.
+- (void)optionsMenuChanged:(id)item {
+ if ([item respondsToSelector:@selector(tag)]) {
+ int cmd = [item tag];
+ // Danger Will Robinson! : This call can release the infobar (e.g. invoking
+ // "About Translate" can open a new tab).
+ // Do not access member variables after this line!
+ optionsMenuModel_->ExecuteCommand(cmd);
+ } else {
+ NOTREACHED();
+ }
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+#pragma mark NSMenuDelegate
+
+// Invoked by virtue of us being set as the delegate for the options menu.
+- (void)menuNeedsUpdate:(NSMenu *)menu {
+ [self adjustOptionsButtonSizeAndVisibilityForView:
+ [[self visibleControls] lastObject]];
+}
+
+@end
+
+@implementation TranslateInfoBarControllerBase (TestingAPI)
+
+- (NSArray*)allControls {
+ return [NSArray arrayWithObjects:label1_.get(),fromLanguagePopUp_.get(),
+ label2_.get(), toLanguagePopUp_.get(), label3_.get(), okButton_,
+ cancelButton_, showOriginalButton_.get(), translateMessageButton_.get(),
+ nil];
+}
+
+- (NSMenu*)optionsMenu {
+ return [optionsPopUp_ menu];
+}
+
+- (NSButton*)translateMessageButton {
+ return translateMessageButton_.get();
+}
+
+- (bool)verifyLayout {
+ // All the controls available to translate infobars, except the options popup.
+ // The options popup is shown/hidden instead of actually removed. This gets
+ // checked in the subclasses.
+ NSArray* allControls = [self allControls];
+ NSArray* visibleControls = [self visibleControls];
+
+ // Step 1: Make sure control visibility is what we expect.
+ for (NSUInteger i = 0; i < [allControls count]; ++i) {
+ id control = [allControls objectAtIndex:i];
+ bool hasSuperView = [control superview];
+ bool expectedVisibility = [visibleControls containsObject:control];
+
+ if (expectedVisibility != hasSuperView) {
+ NSString *title = @"";
+ if ([control isKindOfClass:[NSPopUpButton class]]) {
+ title = [[[control menu] itemAtIndex:0] title];
+ }
+
+ LOG(ERROR) <<
+ "State: " << [self description] <<
+ " Control @" << i << (hasSuperView ? " has" : " doesn't have") <<
+ " a superview" << [[control description] UTF8String] <<
+ " Title=" << [title UTF8String];
+ return false;
+ }
+ }
+
+ // Step 2: Check that controls are ordered correctly with no overlap.
+ id previousControl = nil;
+ for (NSUInteger i = 0; i < [visibleControls count]; ++i) {
+ id control = [visibleControls objectAtIndex:i];
+ // The options pop up doesn't lay out like the rest of the controls as
+ // it floats to the right. It has some known issues shown in
+ // http://crbug.com/47941.
+ if (control == optionsPopUp_.get())
+ continue;
+ if (previousControl &&
+ !VerifyControlOrderAndSpacing(previousControl, control)) {
+ NSString *title = @"";
+ if ([control isKindOfClass:[NSPopUpButton class]]) {
+ title = [[[control menu] itemAtIndex:0] title];
+ }
+ LOG(ERROR) <<
+ "State: " << [self description] <<
+ " Control @" << i << " not ordered correctly: " <<
+ [[control description] UTF8String] <<[title UTF8String];
+ return false;
+ }
+ previousControl = control;
+ }
+
+ return true;
+}
+
+@end // TranslateInfoBarControllerBase (TestingAPI)
+
diff --git a/chrome/browser/ui/cocoa/translate/translate_infobar_unittest.mm b/chrome/browser/ui/cocoa/translate/translate_infobar_unittest.mm
new file mode 100644
index 0000000..9ee57a4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/translate/translate_infobar_unittest.mm
@@ -0,0 +1,254 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/scoped_nsobject.h"
+#import "base/string_util.h"
+#include "base/utf_string_conversions.h"
+#import "chrome/app/chrome_command_ids.h" // For translate menu command ids.
+#import "chrome/browser/renderer_host/site_instance.h"
+#import "chrome/browser/tab_contents/tab_contents.h"
+#import "chrome/browser/translate/translate_infobar_delegate.h"
+#import "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/infobar.h"
+#import "chrome/browser/ui/cocoa/translate/translate_infobar_base.h"
+#import "chrome/browser/ui/cocoa/translate/before_translate_infobar_controller.h"
+#import "testing/gmock/include/gmock/gmock.h"
+#import "testing/gtest/include/gtest/gtest.h"
+#import "testing/platform_test.h"
+
+namespace {
+
+// All states the translate toolbar can assume.
+TranslateInfoBarDelegate::Type kTranslateToolbarStates[] = {
+ TranslateInfoBarDelegate::BEFORE_TRANSLATE,
+ TranslateInfoBarDelegate::AFTER_TRANSLATE,
+ TranslateInfoBarDelegate::TRANSLATING,
+ TranslateInfoBarDelegate::TRANSLATION_ERROR
+};
+
+class MockTranslateInfoBarDelegate : public TranslateInfoBarDelegate {
+ public:
+ MockTranslateInfoBarDelegate(TranslateInfoBarDelegate::Type type,
+ TranslateErrors::Type error,
+ TabContents* contents)
+ : TranslateInfoBarDelegate(type, error, contents, "en", "es"){
+ // Start out in the "Before Translate" state.
+ type_ = type;
+
+ }
+
+ virtual string16 GetDisplayNameForLocale(const std::string& language_code) {
+ return ASCIIToUTF16("Foo");
+ }
+
+ virtual bool IsLanguageBlacklisted() {
+ return false;
+ }
+
+ virtual bool IsSiteBlacklisted() {
+ return false;
+ }
+
+ virtual bool ShouldAlwaysTranslate() {
+ return false;
+ }
+
+ MOCK_METHOD0(Translate, void());
+ MOCK_METHOD0(RevertTranslation, void());
+ MOCK_METHOD0(TranslationDeclined, void());
+ MOCK_METHOD0(ToggleLanguageBlacklist, void());
+ MOCK_METHOD0(ToggleSiteBlacklist, void());
+ MOCK_METHOD0(ToggleAlwaysTranslate, void());
+};
+
+class TranslationInfoBarTest : public CocoaTest {
+ public:
+ BrowserTestHelper browser_helper_;
+ scoped_ptr<TabContents> tab_contents;
+ scoped_ptr<MockTranslateInfoBarDelegate> infobar_delegate;
+ scoped_nsobject<TranslateInfoBarControllerBase> infobar_controller;
+
+ public:
+ // Each test gets a single Mock translate delegate for the lifetime of
+ // the test.
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ tab_contents.reset(
+ new TabContents(browser_helper_.profile(),
+ NULL,
+ MSG_ROUTING_NONE,
+ NULL,
+ NULL));
+ CreateInfoBar();
+ }
+
+ void CreateInfoBar() {
+ CreateInfoBar(TranslateInfoBarDelegate::BEFORE_TRANSLATE);
+ }
+
+ void CreateInfoBar(TranslateInfoBarDelegate::Type type) {
+ TranslateErrors::Type error = TranslateErrors::NONE;
+ if (type == TranslateInfoBarDelegate::TRANSLATION_ERROR)
+ error = TranslateErrors::NETWORK;
+ infobar_delegate.reset(
+ new MockTranslateInfoBarDelegate(type, error, tab_contents.get()));
+ [[infobar_controller view] removeFromSuperview];
+ scoped_ptr<InfoBar> infobar(infobar_delegate->CreateInfoBar());
+ infobar_controller.reset(
+ reinterpret_cast<TranslateInfoBarControllerBase*>(
+ infobar->controller()));
+ // We need to set the window to be wide so that the options button
+ // doesn't overlap the other buttons.
+ [test_window() setContentSize:NSMakeSize(2000, 500)];
+ [[infobar_controller view] setFrame:NSMakeRect(0, 0, 2000, 500)];
+ [[test_window() contentView] addSubview:[infobar_controller view]];
+ }
+};
+
+// Check that we can instantiate a Translate Infobar correctly.
+TEST_F(TranslationInfoBarTest, Instantiate) {
+ CreateInfoBar();
+ ASSERT_TRUE(infobar_controller.get());
+}
+
+// Check that clicking the Translate button calls Translate().
+TEST_F(TranslationInfoBarTest, TranslateCalledOnButtonPress) {
+ CreateInfoBar();
+
+ EXPECT_CALL(*infobar_delegate, Translate()).Times(1);
+ [infobar_controller ok:nil];
+}
+
+// Check that clicking the "Retry" button calls Translate() when we're
+// in the error mode - http://crbug.com/41315 .
+TEST_F(TranslationInfoBarTest, TranslateCalledInErrorMode) {
+ CreateInfoBar(TranslateInfoBarDelegate::TRANSLATION_ERROR);
+
+ EXPECT_CALL(*infobar_delegate, Translate()).Times(1);
+
+ [infobar_controller ok:nil];
+}
+
+// Check that clicking the "Show Original button calls RevertTranslation().
+TEST_F(TranslationInfoBarTest, RevertCalledOnButtonPress) {
+ CreateInfoBar();
+
+ EXPECT_CALL(*infobar_delegate, RevertTranslation()).Times(1);
+ [infobar_controller showOriginal:nil];
+}
+
+// Check that items in the options menu are hooked up correctly.
+TEST_F(TranslationInfoBarTest, OptionsMenuItemsHookedUp) {
+ EXPECT_CALL(*infobar_delegate, Translate())
+ .Times(0);
+
+ [infobar_controller rebuildOptionsMenu:NO];
+ NSMenu* optionsMenu = [infobar_controller optionsMenu];
+ NSArray* optionsMenuItems = [optionsMenu itemArray];
+
+ EXPECT_EQ(7U, [optionsMenuItems count]);
+
+ // First item is the options menu button's title, so there's no need to test
+ // that the target on that is setup correctly.
+ for (NSUInteger i = 1; i < [optionsMenuItems count]; ++i) {
+ NSMenuItem* item = [optionsMenuItems objectAtIndex:i];
+ if (![item isSeparatorItem])
+ EXPECT_EQ([item target], infobar_controller.get());
+ }
+ NSMenuItem* alwaysTranslateLanguateItem = [optionsMenuItems objectAtIndex:1];
+ NSMenuItem* neverTranslateLanguateItem = [optionsMenuItems objectAtIndex:2];
+ NSMenuItem* neverTranslateSiteItem = [optionsMenuItems objectAtIndex:3];
+ // Separator at 4.
+ NSMenuItem* reportBadLanguageItem = [optionsMenuItems objectAtIndex:5];
+ NSMenuItem* aboutTranslateItem = [optionsMenuItems objectAtIndex:6];
+
+ {
+ EXPECT_CALL(*infobar_delegate, ToggleAlwaysTranslate())
+ .Times(1);
+ [infobar_controller optionsMenuChanged:alwaysTranslateLanguateItem];
+ }
+
+ {
+ EXPECT_CALL(*infobar_delegate, ToggleLanguageBlacklist())
+ .Times(1);
+ [infobar_controller optionsMenuChanged:neverTranslateLanguateItem];
+ }
+
+ {
+ EXPECT_CALL(*infobar_delegate, ToggleSiteBlacklist())
+ .Times(1);
+ [infobar_controller optionsMenuChanged:neverTranslateSiteItem];
+ }
+
+ {
+ // Can't mock these effectively, so just check that the tag is set
+ // correctly.
+ EXPECT_EQ(IDC_TRANSLATE_REPORT_BAD_LANGUAGE_DETECTION,
+ [reportBadLanguageItem tag]);
+ EXPECT_EQ(IDC_TRANSLATE_OPTIONS_ABOUT, [aboutTranslateItem tag]);
+ }
+}
+
+// Check that selecting a new item from the "Source Language" popup in "before
+// translate" mode doesn't trigger a translation or change state.
+// http://crbug.com/36666
+TEST_F(TranslationInfoBarTest, Bug36666) {
+ EXPECT_CALL(*infobar_delegate, Translate())
+ .Times(0);
+
+ CreateInfoBar();
+ int arbitrary_index = 2;
+ [infobar_controller sourceLanguageModified:arbitrary_index];
+ EXPECT_CALL(*infobar_delegate, Translate())
+ .Times(0);
+}
+
+// Check that the infobar lays itself out correctly when instantiated in
+// each of the states.
+// http://crbug.com/36895
+TEST_F(TranslationInfoBarTest, Bug36895) {
+ EXPECT_CALL(*infobar_delegate, Translate())
+ .Times(0);
+
+ for (size_t i = 0; i < arraysize(kTranslateToolbarStates); ++i) {
+ CreateInfoBar(kTranslateToolbarStates[i]);
+ EXPECT_TRUE(
+ [infobar_controller verifyLayout]) << "Layout wrong, for state #" << i;
+ }
+}
+
+// Verify that the infobar shows the "Always translate this language" button
+// after doing 3 translations.
+TEST_F(TranslationInfoBarTest, TriggerShowAlwaysTranslateButton) {
+ TranslatePrefs translate_prefs(browser_helper_.profile()->GetPrefs());
+ translate_prefs.ResetTranslationAcceptedCount("en");
+ for (int i = 0; i < 4; ++i) {
+ translate_prefs.IncrementTranslationAcceptedCount("en");
+ }
+ CreateInfoBar(TranslateInfoBarDelegate::BEFORE_TRANSLATE);
+ BeforeTranslateInfobarController* controller =
+ (BeforeTranslateInfobarController*)infobar_controller.get();
+ EXPECT_TRUE([[controller alwaysTranslateButton] superview] != nil);
+ EXPECT_TRUE([[controller neverTranslateButton] superview] == nil);
+}
+
+// Verify that the infobar shows the "Never translate this language" button
+// after denying 3 translations.
+TEST_F(TranslationInfoBarTest, TriggerShowNeverTranslateButton) {
+ TranslatePrefs translate_prefs(browser_helper_.profile()->GetPrefs());
+ translate_prefs.ResetTranslationDeniedCount("en");
+ for (int i = 0; i < 4; ++i) {
+ translate_prefs.IncrementTranslationDeniedCount("en");
+ }
+ CreateInfoBar(TranslateInfoBarDelegate::BEFORE_TRANSLATE);
+ BeforeTranslateInfobarController* controller =
+ (BeforeTranslateInfobarController*)infobar_controller.get();
+ EXPECT_TRUE([[controller alwaysTranslateButton] superview] == nil);
+ EXPECT_TRUE([[controller neverTranslateButton] superview] != nil);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/translate/translate_message_infobar_controller.h b/chrome/browser/ui/cocoa/translate/translate_message_infobar_controller.h
new file mode 100644
index 0000000..8a85403
--- /dev/null
+++ b/chrome/browser/ui/cocoa/translate/translate_message_infobar_controller.h
@@ -0,0 +1,10 @@
+// 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/ui/cocoa/translate/translate_infobar_base.h"
+
+@interface TranslateMessageInfobarController : TranslateInfoBarControllerBase {
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/translate/translate_message_infobar_controller.mm b/chrome/browser/ui/cocoa/translate/translate_message_infobar_controller.mm
new file mode 100644
index 0000000..00ffe2e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/translate/translate_message_infobar_controller.mm
@@ -0,0 +1,53 @@
+// 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 "chrome/browser/ui/cocoa/translate/translate_message_infobar_controller.h"
+
+#include "base/sys_string_conversions.h"
+
+using TranslateInfoBarUtilities::MoveControl;
+
+@implementation TranslateMessageInfobarController
+
+- (void)layout {
+ [self removeOkCancelButtons];
+ MoveControl(label1_, translateMessageButton_, spaceBetweenControls_ * 2, true);
+ TranslateInfoBarDelegate* delegate = [self delegate];
+ if ([self delegate]->ShouldShowMessageInfoBarButton()) {
+ string16 buttonText = delegate->GetMessageInfoBarButtonText();
+ [translateMessageButton_ setTitle:base::SysUTF16ToNSString(buttonText)];
+ [translateMessageButton_ sizeToFit];
+ }
+}
+
+- (void)adjustOptionsButtonSizeAndVisibilityForView:(NSView*)lastView {
+ // Do nothing, but stop the options button from showing up.
+}
+
+- (NSArray*)visibleControls {
+ NSMutableArray* visibleControls =
+ [NSMutableArray arrayWithObjects:label1_.get(), nil];
+ if ([self delegate]->ShouldShowMessageInfoBarButton())
+ [visibleControls addObject:translateMessageButton_];
+ return visibleControls;
+}
+
+- (void)loadLabelText {
+ TranslateInfoBarDelegate* delegate = [self delegate];
+ string16 messageText = delegate->GetMessageInfoBarText();
+ NSString* string1 = base::SysUTF16ToNSString(messageText);
+ [label1_ setStringValue:string1];
+}
+
+- (bool)verifyLayout {
+ if (![optionsPopUp_ isHidden])
+ return false;
+ return [super verifyLayout];
+}
+
+- (BOOL)shouldShowOptionsPopUp {
+ return NO;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/ui_localizer.h b/chrome/browser/ui/cocoa/ui_localizer.h
new file mode 100644
index 0000000..6e426c4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/ui_localizer.h
@@ -0,0 +1,35 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_UI_LOCALIZER_H_
+#define CHROME_BROWSER_UI_COCOA_UI_LOCALIZER_H_
+#pragma once
+
+#import "third_party/GTM/AppKit/GTMUILocalizer.h"
+
+@class NSString;
+
+// A base class for generated localizers.
+//
+// To use this, include your xib file in the list generate_localizer scans (see
+// chrome.gyp). Then add an instance of ChromeUILocalizer to the xib.
+// Connect the owner_ outlet of the instance to the "File's Owner" of the xib.
+// It expects the owner_ outlet to be an instance or subclass of
+// NSWindowController or NSViewController. It will then localize any items in
+// the NSWindowController's window and subviews, or the NSViewController's view
+// and subviews, when awakeFromNib is called on the instance. You can
+// optionally hook up otherObjectToLocalize_ and yetAnotherObjectToLocalize_ and
+// those will also be localized. Strings in the xib that you want localized must
+// start with ^IDS. The value must be a valid resource constant.
+// Things that will be localized are:
+// - Titles and altTitles (for menus, buttons, windows, menuitems, -tabViewItem)
+// - -stringValue (for labels)
+// - tooltips
+// - accessibility help
+// - accessibility descriptions
+// - menus
+@interface ChromeUILocalizer : GTMUILocalizer
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_UI_LOCALIZER_H_
diff --git a/chrome/browser/ui/cocoa/ui_localizer.mm b/chrome/browser/ui/cocoa/ui_localizer.mm
new file mode 100644
index 0000000..fc2e97e
--- /dev/null
+++ b/chrome/browser/ui/cocoa/ui_localizer.mm
@@ -0,0 +1,98 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/ui_localizer.h"
+
+#import <Foundation/Foundation.h>
+
+#include <stdlib.h>
+#include "app/l10n_util.h"
+#include "app/l10n_util_mac.h"
+#include "base/sys_string_conversions.h"
+#include "base/logging.h"
+#include "grit/app_strings.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+
+struct UILocalizerResourceMap {
+ const char* const name;
+ unsigned int label_id;
+ unsigned int label_arg_id;
+};
+
+
+namespace {
+
+// Utility function for bsearch on a ResourceMap table
+int ResourceMapCompare(const void* utf8Void,
+ const void* resourceMapVoid) {
+ const char* utf8_key = reinterpret_cast<const char*>(utf8Void);
+ const UILocalizerResourceMap* res_map =
+ reinterpret_cast<const UILocalizerResourceMap*> (resourceMapVoid);
+ return strcmp(utf8_key, res_map->name);
+}
+
+} // namespace
+
+@interface GTMUILocalizer (PrivateAdditions)
+- (void)localizedObjects;
+@end
+
+@implementation GTMUILocalizer (PrivateAdditions)
+- (void)localizedObjects {
+ // The ivars are private, so this method lets us trigger the localization
+ // from -[ChromeUILocalizer awakeFromNib].
+ [self localizeObject:owner_ recursively:YES];
+ [self localizeObject:otherObjectToLocalize_ recursively:YES];
+ [self localizeObject:yetAnotherObjectToLocalize_ recursively:YES];
+}
+ @end
+
+@implementation ChromeUILocalizer
+
+- (void)awakeFromNib {
+ // The GTM base is bundle based, since don't need the bundle, use this
+ // override to bypass the bundle lookup and directly do the localization
+ // calls.
+ [self localizedObjects];
+}
+
+- (NSString *)localizedStringForString:(NSString *)string {
+
+ // Include the table here so it is a local static. This header provides
+ // kUIResources and kUIResourcesSize.
+#include "ui_localizer_table.h"
+
+ // Look up the string for the resource id to fetch.
+ const char* utf8_key = [string UTF8String];
+ if (utf8_key) {
+ const void* valVoid = bsearch(utf8_key,
+ kUIResources,
+ kUIResourcesSize,
+ sizeof(UILocalizerResourceMap),
+ ResourceMapCompare);
+ const UILocalizerResourceMap* val =
+ reinterpret_cast<const UILocalizerResourceMap*>(valVoid);
+ if (val) {
+ // Do we need to build the string, or just fetch it?
+ if (val->label_arg_id != 0) {
+ const string16 label_arg(l10n_util::GetStringUTF16(val->label_arg_id));
+ return l10n_util::GetNSStringFWithFixup(val->label_id,
+ label_arg);
+ }
+
+ return l10n_util::GetNSStringWithFixup(val->label_id);
+ }
+
+ // Sanity check, there shouldn't be any strings with this id that aren't
+ // in our map.
+ DLOG_IF(WARNING, [string hasPrefix:@"^ID"]) << "Key '" << utf8_key
+ << "' wasn't in the resource map?";
+ }
+
+ // If we didn't find anything, this string doesn't need localizing.
+ return nil;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/url_drop_target.h b/chrome/browser/ui/cocoa/url_drop_target.h
new file mode 100644
index 0000000..abfefb3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/url_drop_target.h
@@ -0,0 +1,71 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_URL_DROP_TARGET_H_
+#define CHROME_BROWSER_UI_COCOA_URL_DROP_TARGET_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+@protocol URLDropTarget;
+@protocol URLDropTargetController;
+
+// Object which coordinates the dropping of URLs on a given view, sending data
+// and updates to a controller.
+@interface URLDropTargetHandler : NSObject {
+ @private
+ NSView<URLDropTarget>* view_; // weak
+}
+
+// Returns an array of drag types that can be handled.
++ (NSArray*)handledDragTypes;
+
+// Initialize the given view, which must implement the |URLDropTarget| (below),
+// to accept drops of URLs.
+- (id)initWithView:(NSView<URLDropTarget>*)view;
+
+// The owner view should implement the following methods by calling the
+// |URLDropTargetHandler|'s version, and leave the others to the default
+// implementation provided by |NSView|/|NSWindow|.
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender;
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender;
+- (void)draggingExited:(id<NSDraggingInfo>)sender;
+- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender;
+
+@end // @interface URLDropTargetHandler
+
+// Protocol which views that are URL drop targets and use |URLDropTargetHandler|
+// must implement.
+@protocol URLDropTarget
+
+// Returns the controller which handles the drop.
+- (id<URLDropTargetController>)urlDropController;
+
+// The following, which come from |NSDraggingDestination|, must be implemented
+// by calling the |URLDropTargetHandler|'s implementations.
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender;
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender;
+- (void)draggingExited:(id<NSDraggingInfo>)sender;
+- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender;
+
+@end // @protocol URLDropTarget
+
+// Protocol for the controller which handles the actual drop data/drop updates.
+@protocol URLDropTargetController
+
+// The given URLs (an |NSArray| of |NSString|s) were dropped in the given view
+// at the given point (in that view's coordinates).
+- (void)dropURLs:(NSArray*)urls inView:(NSView*)view at:(NSPoint)point;
+
+// Dragging is in progress over the owner view (at the given point, in view
+// coordinates) and any indicator of location -- e.g., an arrow -- should be
+// updated/shown.
+- (void)indicateDropURLsInView:(NSView*)view at:(NSPoint)point;
+
+// Dragging is over, and any indicator should be hidden.
+- (void)hideDropURLsIndicatorInView:(NSView*)view;
+
+@end // @protocol URLDropTargetController
+
+#endif // CHROME_BROWSER_UI_COCOA_URL_DROP_TARGET_H_
diff --git a/chrome/browser/ui/cocoa/url_drop_target.mm b/chrome/browser/ui/cocoa/url_drop_target.mm
new file mode 100644
index 0000000..d854775
--- /dev/null
+++ b/chrome/browser/ui/cocoa/url_drop_target.mm
@@ -0,0 +1,98 @@
+// 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/ui/cocoa/url_drop_target.h"
+
+#include "base/basictypes.h"
+#import "third_party/mozilla/NSPasteboard+Utils.h"
+
+@interface URLDropTargetHandler(Private)
+
+// Gets the appropriate drag operation given the |NSDraggingInfo|.
+- (NSDragOperation)getDragOperation:(id<NSDraggingInfo>)sender;
+
+// Tell the window controller to hide the drop indicator.
+- (void)hideIndicator;
+
+@end // @interface URLDropTargetHandler(Private)
+
+@implementation URLDropTargetHandler
+
++ (NSArray*)handledDragTypes {
+ return [NSArray arrayWithObjects:kWebURLsWithTitlesPboardType,
+ NSURLPboardType,
+ NSStringPboardType,
+ NSFilenamesPboardType,
+ nil];
+}
+
+- (id)initWithView:(NSView<URLDropTarget>*)view {
+ if ((self = [super init])) {
+ view_ = view;
+ [view_ registerForDraggedTypes:[URLDropTargetHandler handledDragTypes]];
+ }
+ return self;
+}
+
+// The following four methods implement parts of the |NSDraggingDestination|
+// protocol, which the owner should "forward" to its |URLDropTargetHandler|
+// (us).
+
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
+ return [self getDragOperation:sender];
+}
+
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
+ NSDragOperation dragOp = [self getDragOperation:sender];
+ if (dragOp == NSDragOperationCopy) {
+ // Just tell the window controller to update the indicator.
+ NSPoint hoverPoint = [view_ convertPoint:[sender draggingLocation]
+ fromView:nil];
+ [[view_ urlDropController] indicateDropURLsInView:view_ at:hoverPoint];
+ }
+ return dragOp;
+}
+
+- (void)draggingExited:(id<NSDraggingInfo>)sender {
+ [self hideIndicator];
+}
+
+- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
+ [self hideIndicator];
+
+ NSPasteboard* pboard = [sender draggingPasteboard];
+ if ([pboard containsURLData]) {
+ NSArray* urls = nil;
+ NSArray* titles; // discarded
+ [pboard getURLs:&urls andTitles:&titles convertingFilenames:YES];
+
+ if ([urls count]) {
+ // Tell the window controller about the dropped URL(s).
+ NSPoint dropPoint =
+ [view_ convertPoint:[sender draggingLocation] fromView:nil];
+ [[view_ urlDropController] dropURLs:urls inView:view_ at:dropPoint];
+ return YES;
+ }
+ }
+
+ return NO;
+}
+
+@end // @implementation URLDropTargetHandler
+
+@implementation URLDropTargetHandler(Private)
+
+- (NSDragOperation)getDragOperation:(id<NSDraggingInfo>)sender {
+ if (![[sender draggingPasteboard] containsURLData])
+ return NSDragOperationNone;
+
+ // Only allow the copy operation.
+ return [sender draggingSourceOperationMask] & NSDragOperationCopy;
+}
+
+- (void)hideIndicator {
+ [[view_ urlDropController] hideDropURLsIndicatorInView:view_];
+}
+
+@end // @implementation URLDropTargetHandler(Private)
diff --git a/chrome/browser/ui/cocoa/vertical_gradient_view.h b/chrome/browser/ui/cocoa/vertical_gradient_view.h
new file mode 100644
index 0000000..98a3a2b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/vertical_gradient_view.h
@@ -0,0 +1,36 @@
+// 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_UI_COCOA_VERTICAL_GRADIENT_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_VERTICAL_GRADIENT_VIEW_H_
+#pragma once
+
+#include "base/scoped_nsobject.h"
+
+#import <Cocoa/Cocoa.h>
+
+// Draws a vertical background gradient with a bottom stroke. The gradient and
+// stroke colors can be defined by calling |setGradient| and |setStrokeColor|,
+// respectively. Alternatively, you may override the |gradient| and
+// |strokeColor| accessors in order to provide colors dynamically. If the
+// gradient or color is |nil|, the respective element will not be drawn.
+@interface VerticalGradientView : NSView {
+ @private
+ // The gradient to draw.
+ scoped_nsobject<NSGradient> gradient_;
+ // Color for bottom stroke.
+ scoped_nsobject<NSColor> strokeColor_;
+}
+
+// Gets and sets the gradient to paint as background.
+- (NSGradient*)gradient;
+- (void)setGradient:(NSGradient*)gradient;
+
+// Gets and sets the color of the stroke drawn at the bottom of the view.
+- (NSColor*)strokeColor;
+- (void)setStrokeColor:(NSColor*)gradient;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_VERTICAL_GRADIENT_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/vertical_gradient_view.mm b/chrome/browser/ui/cocoa/vertical_gradient_view.mm
new file mode 100644
index 0000000..30b9e2f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/vertical_gradient_view.mm
@@ -0,0 +1,39 @@
+// 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 "chrome/browser/ui/cocoa/vertical_gradient_view.h"
+
+@implementation VerticalGradientView
+
+- (NSGradient*)gradient {
+ return gradient_;
+}
+
+- (void)setGradient:(NSGradient*)gradient {
+ gradient_.reset([gradient retain]);
+}
+
+- (NSColor*)strokeColor {
+ return strokeColor_;
+}
+
+- (void)setStrokeColor:(NSColor*)strokeColor {
+ strokeColor_.reset([strokeColor retain]);
+}
+
+- (void)drawRect:(NSRect)rect {
+ // Draw gradient.
+ [[self gradient] drawInRect:[self bounds] angle:270];
+
+ // Draw bottom stroke.
+ NSColor* strokeColor = [self strokeColor];
+ if (strokeColor) {
+ [[self strokeColor] set];
+ NSRect borderRect, contentRect;
+ NSDivideRect([self bounds], &borderRect, &contentRect, 1, NSMinYEdge);
+ NSRectFillUsingOperation(borderRect, NSCompositeSourceOver);
+ }
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/vertical_gradient_view_unittest.mm b/chrome/browser/ui/cocoa/vertical_gradient_view_unittest.mm
new file mode 100644
index 0000000..e574a69
--- /dev/null
+++ b/chrome/browser/ui/cocoa/vertical_gradient_view_unittest.mm
@@ -0,0 +1,27 @@
+// 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/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/vertical_gradient_view.h"
+
+namespace {
+
+class VerticalGradientViewTest : public CocoaTest {
+ public:
+ VerticalGradientViewTest() {
+ NSRect frame = NSMakeRect(0, 0, 50, 27);
+ scoped_nsobject<VerticalGradientView> view(
+ [[VerticalGradientView alloc] initWithFrame:frame]);
+ view_ = view.get();
+ [[test_window() contentView] addSubview:view_];
+ }
+
+ VerticalGradientView* view_;
+};
+
+TEST_VIEW(VerticalGradientViewTest, view_);
+
+} // namespace
+
diff --git a/chrome/browser/ui/cocoa/view_id_util.h b/chrome/browser/ui/cocoa/view_id_util.h
new file mode 100644
index 0000000..e4ca62c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/view_id_util.h
@@ -0,0 +1,52 @@
+// 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_UI_COCOA_VIEW_ID_UTIL_H_
+#define CHROME_BROWSER_UI_COCOA_VIEW_ID_UTIL_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "gfx/native_widget_types.h"
+#include "chrome/browser/view_ids.h"
+
+// ViewIDs are a system that indexes important views in the browser window by a
+// ViewID identifier (integer). This is a useful compatibility for finding a
+// view object in cross-platform tests. See BrowserFocusTest.* for an example
+// of how ViewIDs are used.
+
+// For views with fixed ViewIDs, we add a -viewID method to them to return their
+// ViewIDs directly. But for views with changeable ViewIDs, as NSView itself
+// doesn't provide a facility to store its ViewID, to avoid modifying each
+// individual classes for adding ViewID support, we use an internal map to store
+// ViewIDs of each view and provide some utility functions for NSView to
+// set/unset the ViewID and lookup a view with a specified ViewID.
+
+namespace view_id_util {
+
+// Associates the given ViewID with the view. It shall be called upon the view's
+// initialization.
+void SetID(NSView* view, ViewID viewID);
+
+// Removes the association between the view and its ViewID. It shall be called
+// just before the view's destruction.
+void UnsetID(NSView* view);
+
+// Returns the view with a specific ViewID in a window, or nil if no view in the
+// window has that ViewID.
+NSView* GetView(NSWindow* window, ViewID viewID);
+
+} // namespace view_id_util
+
+
+@interface NSView (ViewID)
+
+// Returns the ViewID associated to the receiver. The default implementation
+// looks up the view's ViewID in the internal view to ViewID map. A subclass may
+// override this method to return its fixed ViewID.
+- (ViewID)viewID;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_VIEW_ID_UTIL_H_
diff --git a/chrome/browser/ui/cocoa/view_id_util.mm b/chrome/browser/ui/cocoa/view_id_util.mm
new file mode 100644
index 0000000..df8f079
--- /dev/null
+++ b/chrome/browser/ui/cocoa/view_id_util.mm
@@ -0,0 +1,87 @@
+// 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/ui/cocoa/view_id_util.h"
+
+#import <Cocoa/Cocoa.h>
+
+#include <map>
+#include <utility>
+
+#include "base/logging.h"
+#include "base/singleton.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/tab_strip_controller.h"
+
+namespace {
+
+// TODO(suzhe): After migrating to Mac OS X 10.6, we may use Objective-C's new
+// "Associative References" feature to attach the ViewID to the view directly
+// rather than using a separated map.
+typedef std::map<NSView*, ViewID> ViewIDMap;
+
+// Returns the view's nearest descendant (including itself) with a specific
+// ViewID, or nil if no subview has that ViewID.
+NSView* FindViewWithID(NSView* view, ViewID viewID) {
+ if ([view viewID] == viewID)
+ return view;
+
+ for (NSView* subview in [view subviews]) {
+ NSView* result = FindViewWithID(subview, viewID);
+ if (result != nil)
+ return result;
+ }
+ return nil;
+}
+
+} // anonymous namespace
+
+namespace view_id_util {
+
+void SetID(NSView* view, ViewID viewID) {
+ DCHECK(view);
+ DCHECK(viewID != VIEW_ID_NONE);
+ // We handle VIEW_ID_TAB_0 to VIEW_ID_TAB_LAST in GetView() function directly.
+ DCHECK(!((viewID >= VIEW_ID_TAB_0) && (viewID <= VIEW_ID_TAB_LAST)));
+ (*Singleton<ViewIDMap>::get())[view] = viewID;
+}
+
+void UnsetID(NSView* view) {
+ DCHECK(view);
+ Singleton<ViewIDMap>::get()->erase(view);
+}
+
+NSView* GetView(NSWindow* window, ViewID viewID) {
+ DCHECK(viewID != VIEW_ID_NONE);
+ DCHECK(window);
+
+ // As tabs can be created, destroyed or rearranged dynamically, we handle them
+ // here specially.
+ if (viewID >= VIEW_ID_TAB_0 && viewID <= VIEW_ID_TAB_LAST) {
+ BrowserWindowController* windowController = [window windowController];
+ DCHECK([windowController isKindOfClass:[BrowserWindowController class]]);
+ TabStripController* tabStripController =
+ [windowController tabStripController];
+ DCHECK(tabStripController);
+ NSUInteger count = [tabStripController viewsCount];
+ DCHECK(count);
+ NSUInteger index =
+ (viewID == VIEW_ID_TAB_LAST ? count - 1 : viewID - VIEW_ID_TAB_0);
+ return index < count ? [tabStripController viewAtIndex:index] : nil;
+ }
+
+ return FindViewWithID([[window contentView] superview], viewID);
+}
+
+} // namespace view_id_util
+
+@implementation NSView (ViewID)
+
+- (ViewID)viewID {
+ ViewIDMap* map = Singleton<ViewIDMap>::get();
+ ViewIDMap::const_iterator iter = map->find(self);
+ return iter != map->end() ? iter->second : VIEW_ID_NONE;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/view_id_util_browsertest.mm b/chrome/browser/ui/cocoa/view_id_util_browsertest.mm
new file mode 100644
index 0000000..ff4c35d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/view_id_util_browsertest.mm
@@ -0,0 +1,118 @@
+// 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/basictypes.h"
+#include "base/command_line.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/download/download_shelf.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/sidebar/sidebar_manager.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/ui/cocoa/view_id_util.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/url_constants.h"
+#include "chrome/test/in_process_browser_test.h"
+#include "chrome/test/ui_test_utils.h"
+
+// Basic sanity check of ViewID use on the mac.
+class ViewIDTest : public InProcessBrowserTest {
+ public:
+ ViewIDTest() : root_window_(nil) {
+ CommandLine::ForCurrentProcess()->AppendSwitch(
+ switches::kEnableExperimentalExtensionApis);
+ }
+
+ void CheckViewID(ViewID view_id, bool should_have) {
+ if (!root_window_)
+ root_window_ = browser()->window()->GetNativeHandle();
+
+ ASSERT_TRUE(root_window_);
+ NSView* view = view_id_util::GetView(root_window_, view_id);
+ EXPECT_EQ(should_have, !!view) << " Failed id=" << view_id;
+ }
+
+ void DoTest() {
+ // Make sure FindBar is created to test
+ // VIEW_ID_FIND_IN_PAGE_TEXT_FIELD and VIEW_ID_FIND_IN_PAGE.
+ browser()->ShowFindBar();
+
+ // Make sure sidebar is created to test VIEW_ID_SIDE_BAR_CONTAINER.
+ const char sidebar_content_id[] = "test_content_id";
+ SidebarManager::GetInstance()->ShowSidebar(
+ browser()->GetSelectedTabContents(), sidebar_content_id);
+ SidebarManager::GetInstance()->ExpandSidebar(
+ browser()->GetSelectedTabContents(), sidebar_content_id);
+
+ // Make sure docked devtools is created to test VIEW_ID_DEV_TOOLS_DOCKED
+ browser()->profile()->GetPrefs()->SetBoolean(prefs::kDevToolsOpenDocked,
+ true);
+ browser()->ToggleDevToolsWindow(DEVTOOLS_TOGGLE_ACTION_INSPECT);
+
+ // Make sure download shelf is created to test VIEW_ID_DOWNLOAD_SHELF
+ browser()->window()->GetDownloadShelf()->Show();
+
+ // Create a bookmark to test VIEW_ID_BOOKMARK_BAR_ELEMENT
+ BookmarkModel* bookmark_model = browser()->profile()->GetBookmarkModel();
+ if (bookmark_model) {
+ if (!bookmark_model->IsLoaded())
+ ui_test_utils::WaitForBookmarkModelToLoad(bookmark_model);
+
+ bookmark_model->SetURLStarred(GURL(chrome::kAboutBlankURL),
+ UTF8ToUTF16("about"), true);
+ }
+
+ for (int i = VIEW_ID_TOOLBAR; i < VIEW_ID_PREDEFINED_COUNT; ++i) {
+ // Mac implementation does not support following ids yet.
+ if (i == VIEW_ID_STAR_BUTTON ||
+ i == VIEW_ID_AUTOCOMPLETE ||
+ i == VIEW_ID_CONTENTS_SPLIT ||
+ i == VIEW_ID_SIDE_BAR_SPLIT ||
+ i == VIEW_ID_FEEDBACK_BUTTON) {
+ continue;
+ }
+
+ CheckViewID(static_cast<ViewID>(i), true);
+ }
+
+ CheckViewID(VIEW_ID_TAB, true);
+ CheckViewID(VIEW_ID_TAB_STRIP, true);
+ CheckViewID(VIEW_ID_PREDEFINED_COUNT, false);
+ }
+
+ private:
+ NSWindow* root_window_;
+};
+
+IN_PROC_BROWSER_TEST_F(ViewIDTest, Basic) {
+ ASSERT_NO_FATAL_FAILURE(DoTest());
+}
+
+IN_PROC_BROWSER_TEST_F(ViewIDTest, Fullscreen) {
+ browser()->window()->SetFullscreen(true);
+ ASSERT_NO_FATAL_FAILURE(DoTest());
+}
+
+IN_PROC_BROWSER_TEST_F(ViewIDTest, Tab) {
+ CheckViewID(VIEW_ID_TAB_0, true);
+ CheckViewID(VIEW_ID_TAB_LAST, true);
+
+ // Open 9 new tabs.
+ for (int i = 1; i <= 9; ++i) {
+ CheckViewID(static_cast<ViewID>(VIEW_ID_TAB_0 + i), false);
+ browser()->OpenURL(GURL(chrome::kAboutBlankURL), GURL(),
+ NEW_BACKGROUND_TAB, PageTransition::TYPED);
+ CheckViewID(static_cast<ViewID>(VIEW_ID_TAB_0 + i), true);
+ // VIEW_ID_TAB_LAST should always be available.
+ CheckViewID(VIEW_ID_TAB_LAST, true);
+ }
+
+ // Open the 11th tab.
+ browser()->OpenURL(GURL(chrome::kAboutBlankURL), GURL(),
+ NEW_BACKGROUND_TAB, PageTransition::TYPED);
+ CheckViewID(VIEW_ID_TAB_LAST, true);
+}
diff --git a/chrome/browser/ui/cocoa/view_resizer.h b/chrome/browser/ui/cocoa/view_resizer.h
new file mode 100644
index 0000000..f27373f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/view_resizer.h
@@ -0,0 +1,28 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_VIEW_RESIZER_H_
+#define CHROME_BROWSER_UI_COCOA_VIEW_RESIZER_H_
+#pragma once
+
+#include "chrome/browser/tabs/tab_strip_model.h"
+
+#import <Cocoa/Cocoa.h>
+
+// Defines a protocol that allows controllers to delegate resizing their views
+// to their parents. When a controller needs to change a view's height, rather
+// than resizing it directly, it sends a message to its parent asking the parent
+// to perform the resize. This allows the parent to do any re-layout that may
+// become necessary due to the resize.
+@protocol ViewResizer <NSObject>
+- (void)resizeView:(NSView*)view newHeight:(CGFloat)height;
+
+@optional
+// Optional method called when an animation is beginning or ending. Resize
+// delegates can implement this method if they need to modify their behavior
+// while an animation is running.
+- (void)setAnimationInProgress:(BOOL)inProgress;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_VIEW_RESIZER_H_
diff --git a/chrome/browser/ui/cocoa/view_resizer_pong.h b/chrome/browser/ui/cocoa/view_resizer_pong.h
new file mode 100644
index 0000000..628c0d5
--- /dev/null
+++ b/chrome/browser/ui/cocoa/view_resizer_pong.h
@@ -0,0 +1,22 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_VIEW_RESIZER_PONG_H_
+#define CHROME_BROWSER_UI_COCOA_VIEW_RESIZER_PONG_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/view_resizer.h"
+
+@interface ViewResizerPong : NSObject<ViewResizer> {
+ @private
+ CGFloat height_;
+}
+@property (nonatomic) CGFloat height;
+
+- (void)resizeView:(NSView*)view newHeight:(CGFloat)height;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_VIEW_RESIZER_PONG_H_
diff --git a/chrome/browser/ui/cocoa/view_resizer_pong.mm b/chrome/browser/ui/cocoa/view_resizer_pong.mm
new file mode 100644
index 0000000..f063dbf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/view_resizer_pong.mm
@@ -0,0 +1,20 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/view_resizer_pong.h"
+
+@implementation ViewResizerPong
+
+@synthesize height = height_;
+
+- (void)resizeView:(NSView*)view newHeight:(CGFloat)height {
+ [self setHeight:height];
+
+ // Set the view's height and width, in case it uses that as important state.
+ [view setFrame:NSMakeRect(100, 50,
+ NSWidth([[view superview] frame]) - 50, height)];
+}
+@end
diff --git a/chrome/browser/ui/cocoa/web_contents_drag_source.h b/chrome/browser/ui/cocoa/web_contents_drag_source.h
new file mode 100644
index 0000000..aa9dd73
--- /dev/null
+++ b/chrome/browser/ui/cocoa/web_contents_drag_source.h
@@ -0,0 +1,62 @@
+// 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_UI_COCOA_WEB_CONTENTS_DRAG_SOURCE_H_
+#define CHROME_BROWSER_UI_COCOA_WEB_CONTENTS_DRAG_SOURCE_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/bookmarks/bookmark_node_data.h"
+
+@class TabContentsViewCocoa;
+
+// A class that handles tracking and event processing for a drag and drop
+// originating from the content area. Subclasses should implement
+// fillClipboard and dragImage.
+@interface WebContentsDragSource : NSObject {
+ @private
+ // Our tab. Weak reference (owns or co-owns us).
+ TabContentsViewCocoa* contentsView_;
+
+ // Our pasteboard.
+ scoped_nsobject<NSPasteboard> pasteboard_;
+
+ // A mask of the allowed drag operations.
+ NSDragOperation dragOperationMask_;
+}
+
+// Initialize a DragDataSource object for a drag (originating on the given
+// contentsView and with the given dropData and pboard). Fill the pasteboard
+// with data types appropriate for dropData.
+- (id)initWithContentsView:(TabContentsViewCocoa*)contentsView
+ pasteboard:(NSPasteboard*)pboard
+ dragOperationMask:(NSDragOperation)dragOperationMask;
+
+// Creates the drag image. Implemented by the subclass.
+- (NSImage*)dragImage;
+
+// Put the data being dragged onto the pasteboard. Implemented by the
+// subclass.
+- (void)fillPasteboard;
+
+// Returns a mask of the allowed drag operations.
+- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal;
+
+// Start the drag (on the originally provided contentsView); can do this right
+// after -initWithContentsView:....
+- (void)startDrag;
+
+// End the drag and clear the pasteboard; hook up to
+// -draggedImage:endedAt:operation:.
+- (void)endDragAt:(NSPoint)screenPoint
+ operation:(NSDragOperation)operation;
+
+// Drag moved; hook up to -draggedImage:movedTo:.
+- (void)moveDragTo:(NSPoint)screenPoint;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_WEB_CONTENTS_DRAG_SOURCE_H_
diff --git a/chrome/browser/ui/cocoa/web_contents_drag_source.mm b/chrome/browser/ui/cocoa/web_contents_drag_source.mm
new file mode 100644
index 0000000..072909a
--- /dev/null
+++ b/chrome/browser/ui/cocoa/web_contents_drag_source.mm
@@ -0,0 +1,130 @@
+// 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/ui/cocoa/web_contents_drag_source.h"
+
+#include "base/nsimage_cache_mac.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/renderer_host/render_view_host.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents/tab_contents_view_mac.h"
+
+namespace {
+
+// Make a drag image from the drop data.
+// TODO(feldstein): Make this work
+NSImage* MakeDragImage() {
+ // TODO(feldstein): Just a stub for now. Make it do something (see, e.g.,
+ // WebKit/WebKit/mac/Misc/WebNSViewExtras.m: |-_web_DragImageForElement:...|).
+
+ // Default to returning a generic image.
+ return nsimage_cache::ImageNamed(@"nav.pdf");
+}
+
+// Flips screen and point coordinates over the y axis to work with webkit
+// coordinate systems.
+void FlipPointCoordinates(NSPoint& screenPoint,
+ NSPoint& localPoint,
+ NSView* view) {
+ NSRect viewFrame = [view frame];
+ localPoint.y = NSHeight(viewFrame) - localPoint.y;
+ // Flip |screenPoint|.
+ NSRect screenFrame = [[[view window] screen] frame];
+ screenPoint.y = NSHeight(screenFrame) - screenPoint.y;
+}
+
+} // namespace
+
+
+@implementation WebContentsDragSource
+
+- (id)initWithContentsView:(TabContentsViewCocoa*)contentsView
+ pasteboard:(NSPasteboard*)pboard
+ dragOperationMask:(NSDragOperation)dragOperationMask {
+ if ((self = [super init])) {
+ contentsView_ = contentsView;
+ DCHECK(contentsView_);
+
+ pasteboard_.reset([pboard retain]);
+ DCHECK(pasteboard_.get());
+
+ dragOperationMask_ = dragOperationMask;
+ }
+
+ return self;
+}
+
+- (NSImage*)dragImage {
+ return MakeDragImage();
+}
+
+- (void)fillPasteboard {
+ NOTIMPLEMENTED() << "Subclasses should implement fillPasteboard";
+}
+
+- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
+ return dragOperationMask_;
+}
+
+- (void)startDrag {
+ [self fillPasteboard];
+ NSEvent* currentEvent = [NSApp currentEvent];
+
+ // Synthesize an event for dragging, since we can't be sure that
+ // [NSApp currentEvent] will return a valid dragging event.
+ NSWindow* window = [contentsView_ window];
+ NSPoint position = [window mouseLocationOutsideOfEventStream];
+ NSTimeInterval eventTime = [currentEvent timestamp];
+ NSEvent* dragEvent = [NSEvent mouseEventWithType:NSLeftMouseDragged
+ location:position
+ modifierFlags:NSLeftMouseDraggedMask
+ timestamp:eventTime
+ windowNumber:[window windowNumber]
+ context:nil
+ eventNumber:0
+ clickCount:1
+ pressure:1.0];
+ [window dragImage:[self dragImage]
+ at:position
+ offset:NSZeroSize
+ event:dragEvent
+ pasteboard:pasteboard_
+ source:self
+ slideBack:YES];
+}
+
+- (void)draggedImage:(NSImage *)anImage endedAt:(NSPoint)aPoint
+ operation:(NSDragOperation)operation {
+}
+
+- (void)endDragAt:(NSPoint)screenPoint
+ operation:(NSDragOperation)operation {
+ RenderViewHost* rvh = [contentsView_ tabContents]->render_view_host();
+ if (rvh) {
+ rvh->DragSourceSystemDragEnded();
+
+ NSPoint localPoint = [contentsView_ convertPoint:screenPoint fromView: nil];
+ FlipPointCoordinates(screenPoint, localPoint, contentsView_);
+ rvh->DragSourceEndedAt(localPoint.x, localPoint.y,
+ screenPoint.x, screenPoint.y,
+ static_cast<WebKit::WebDragOperation>(operation));
+ }
+
+ // Make sure the pasteboard owner isn't us.
+ [pasteboard_ declareTypes:[NSArray array] owner:nil];
+}
+
+- (void)moveDragTo:(NSPoint)screenPoint {
+ RenderViewHost* rvh = [contentsView_ tabContents]->render_view_host();
+ if (rvh) {
+ NSPoint localPoint = [contentsView_ convertPoint:screenPoint fromView:nil];
+ FlipPointCoordinates(screenPoint, localPoint, contentsView_);
+ rvh->DragSourceMovedTo(localPoint.x, localPoint.y,
+ screenPoint.x, screenPoint.y);
+ }
+}
+
+@end // @implementation WebContentsDragSource
+
diff --git a/chrome/browser/ui/cocoa/web_drag_source.h b/chrome/browser/ui/cocoa/web_drag_source.h
new file mode 100644
index 0000000..9cfcba5
--- /dev/null
+++ b/chrome/browser/ui/cocoa/web_drag_source.h
@@ -0,0 +1,80 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/file_path.h"
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "googleurl/src/gurl.h"
+
+@class TabContentsViewCocoa;
+struct WebDropData;
+
+// A class that handles tracking and event processing for a drag and drop
+// originating from the content area.
+@interface WebDragSource : NSObject {
+ @private
+ // Our tab. Weak reference (owns or co-owns us).
+ TabContentsViewCocoa* contentsView_;
+
+ // Our drop data. Should only be initialized once.
+ scoped_ptr<WebDropData> dropData_;
+
+ // The image to show as drag image. Can be nil.
+ scoped_nsobject<NSImage> dragImage_;
+
+ // The offset to draw |dragImage_| at.
+ NSPoint imageOffset_;
+
+ // Our pasteboard.
+ scoped_nsobject<NSPasteboard> pasteboard_;
+
+ // A mask of the allowed drag operations.
+ NSDragOperation dragOperationMask_;
+
+ // The file name to be saved to for a drag-out download.
+ FilePath downloadFileName_;
+
+ // The URL to download from for a drag-out download.
+ GURL downloadURL_;
+}
+
+// Initialize a WebDragSource object for a drag (originating on the given
+// contentsView and with the given dropData and pboard). Fill the pasteboard
+// with data types appropriate for dropData.
+- (id)initWithContentsView:(TabContentsViewCocoa*)contentsView
+ dropData:(const WebDropData*)dropData
+ image:(NSImage*)image
+ offset:(NSPoint)offset
+ pasteboard:(NSPasteboard*)pboard
+ dragOperationMask:(NSDragOperation)dragOperationMask;
+
+// Returns a mask of the allowed drag operations.
+- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal;
+
+// Call when asked to do a lazy write to the pasteboard; hook up to
+// -pasteboard:provideDataForType: (on the contentsView).
+- (void)lazyWriteToPasteboard:(NSPasteboard*)pboard
+ forType:(NSString*)type;
+
+// Start the drag (on the originally provided contentsView); can do this right
+// after -initWithContentsView:....
+- (void)startDrag;
+
+// End the drag and clear the pasteboard; hook up to
+// -draggedImage:endedAt:operation:.
+- (void)endDragAt:(NSPoint)screenPoint
+ operation:(NSDragOperation)operation;
+
+// Drag moved; hook up to -draggedImage:movedTo:.
+- (void)moveDragTo:(NSPoint)screenPoint;
+
+// Call to drag a promised file to the given path (should be called before
+// -endDragAt:...); hook up to -namesOfPromisedFilesDroppedAtDestination:.
+// Returns the file name (not including path) of the file deposited (or which
+// will be deposited).
+- (NSString*)dragPromisedFileTo:(NSString*)path;
+
+@end
diff --git a/chrome/browser/ui/cocoa/web_drag_source.mm b/chrome/browser/ui/cocoa/web_drag_source.mm
new file mode 100644
index 0000000..cad5014
--- /dev/null
+++ b/chrome/browser/ui/cocoa/web_drag_source.mm
@@ -0,0 +1,412 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/web_drag_source.h"
+
+#include "base/file_path.h"
+#include "base/nsimage_cache_mac.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "base/task.h"
+#include "base/thread.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/download/download_manager.h"
+#include "chrome/browser/download/download_util.h"
+#include "chrome/browser/download/drag_download_file.h"
+#include "chrome/browser/download/drag_download_util.h"
+#include "chrome/browser/renderer_host/render_view_host.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents/tab_contents_view_mac.h"
+#include "net/base/file_stream.h"
+#include "net/base/net_util.h"
+#import "third_party/mozilla/NSPasteboard+Utils.h"
+#include "webkit/glue/webdropdata.h"
+
+using base::SysNSStringToUTF8;
+using base::SysUTF8ToNSString;
+using base::SysUTF16ToNSString;
+using net::FileStream;
+
+
+namespace {
+
+// An unofficial standard pasteboard title type to be provided alongside the
+// |NSURLPboardType|.
+NSString* const kNSURLTitlePboardType = @"public.url-name";
+
+// Returns a filename appropriate for the drop data
+// TODO(viettrungluu): Refactor to make it common across platforms,
+// and move it somewhere sensible.
+FilePath GetFileNameFromDragData(const WebDropData& drop_data) {
+ // Images without ALT text will only have a file extension so we need to
+ // synthesize one from the provided extension and URL.
+ FilePath file_name([SysUTF16ToNSString(drop_data.file_description_filename)
+ fileSystemRepresentation]);
+ file_name = file_name.BaseName().RemoveExtension();
+
+ if (file_name.empty()) {
+ // Retrieve the name from the URL.
+ file_name = net::GetSuggestedFilename(drop_data.url, "", "", FilePath());
+ }
+
+ file_name = file_name.ReplaceExtension([SysUTF16ToNSString(
+ drop_data.file_extension) fileSystemRepresentation]);
+
+ return file_name;
+}
+
+// This class's sole task is to write out data for a promised file; the caller
+// is responsible for opening the file.
+class PromiseWriterTask : public Task {
+ public:
+ // Assumes ownership of file_stream.
+ PromiseWriterTask(const WebDropData& drop_data,
+ FileStream* file_stream);
+ virtual ~PromiseWriterTask();
+ virtual void Run();
+
+ private:
+ WebDropData drop_data_;
+
+ // This class takes ownership of file_stream_ and will close and delete it.
+ scoped_ptr<FileStream> file_stream_;
+};
+
+// Takes the drop data and an open file stream (which it takes ownership of and
+// will close and delete).
+PromiseWriterTask::PromiseWriterTask(const WebDropData& drop_data,
+ FileStream* file_stream) :
+ drop_data_(drop_data) {
+ file_stream_.reset(file_stream);
+ DCHECK(file_stream_.get());
+}
+
+PromiseWriterTask::~PromiseWriterTask() {
+ DCHECK(file_stream_.get());
+ if (file_stream_.get())
+ file_stream_->Close();
+}
+
+void PromiseWriterTask::Run() {
+ CHECK(file_stream_.get());
+ file_stream_->Write(drop_data_.file_contents.data(),
+ drop_data_.file_contents.length(),
+ NULL);
+
+ // Let our destructor take care of business.
+}
+
+} // namespace
+
+
+@interface WebDragSource(Private)
+
+- (void)fillPasteboard;
+- (NSImage*)dragImage;
+
+@end // @interface WebDragSource(Private)
+
+
+@implementation WebDragSource
+
+- (id)initWithContentsView:(TabContentsViewCocoa*)contentsView
+ dropData:(const WebDropData*)dropData
+ image:(NSImage*)image
+ offset:(NSPoint)offset
+ pasteboard:(NSPasteboard*)pboard
+ dragOperationMask:(NSDragOperation)dragOperationMask {
+ if ((self = [super init])) {
+ contentsView_ = contentsView;
+ DCHECK(contentsView_);
+
+ dropData_.reset(new WebDropData(*dropData));
+ DCHECK(dropData_.get());
+
+ dragImage_.reset([image retain]);
+ imageOffset_ = offset;
+
+ pasteboard_.reset([pboard retain]);
+ DCHECK(pasteboard_.get());
+
+ dragOperationMask_ = dragOperationMask;
+
+ [self fillPasteboard];
+ }
+
+ return self;
+}
+
+- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal {
+ return dragOperationMask_;
+}
+
+- (void)lazyWriteToPasteboard:(NSPasteboard*)pboard forType:(NSString*)type {
+ // NSHTMLPboardType requires the character set to be declared. Otherwise, it
+ // assumes US-ASCII. Awesome.
+ static const string16 kHtmlHeader =
+ ASCIIToUTF16("<meta http-equiv=\"Content-Type\" "
+ "content=\"text/html;charset=UTF-8\">");
+
+ // Be extra paranoid; avoid crashing.
+ if (!dropData_.get()) {
+ NOTREACHED() << "No drag-and-drop data available for lazy write.";
+ return;
+ }
+
+ // HTML.
+ if ([type isEqualToString:NSHTMLPboardType]) {
+ DCHECK(!dropData_->text_html.empty());
+ // See comment on |kHtmlHeader| above.
+ [pboard setString:SysUTF16ToNSString(kHtmlHeader + dropData_->text_html)
+ forType:NSHTMLPboardType];
+
+ // URL.
+ } else if ([type isEqualToString:NSURLPboardType]) {
+ DCHECK(dropData_->url.is_valid());
+ NSURL* url = [NSURL URLWithString:SysUTF8ToNSString(dropData_->url.spec())];
+ [url writeToPasteboard:pboard];
+
+ // URL title.
+ } else if ([type isEqualToString:kNSURLTitlePboardType]) {
+ [pboard setString:SysUTF16ToNSString(dropData_->url_title)
+ forType:kNSURLTitlePboardType];
+
+ // File contents.
+ } else if ([type isEqualToString:NSFileContentsPboardType] ||
+ [type isEqualToString:NSCreateFileContentsPboardType(
+ SysUTF16ToNSString(dropData_->file_extension))]) {
+ // TODO(viettrungluu: find something which is known to accept
+ // NSFileContentsPboardType to check that this actually works!
+ scoped_nsobject<NSFileWrapper> file_wrapper(
+ [[NSFileWrapper alloc] initRegularFileWithContents:[NSData
+ dataWithBytes:dropData_->file_contents.data()
+ length:dropData_->file_contents.length()]]);
+ [file_wrapper setPreferredFilename:SysUTF8ToNSString(
+ GetFileNameFromDragData(*dropData_).value())];
+ [pboard writeFileWrapper:file_wrapper];
+
+ // TIFF.
+ } else if ([type isEqualToString:NSTIFFPboardType]) {
+ // TODO(viettrungluu): This is a bit odd since we rely on Cocoa to render
+ // our image into a TIFF. This is also suboptimal since this is all done
+ // synchronously. I'm not sure there's much we can easily do about it.
+ scoped_nsobject<NSImage> image(
+ [[NSImage alloc] initWithData:[NSData
+ dataWithBytes:dropData_->file_contents.data()
+ length:dropData_->file_contents.length()]]);
+ [pboard setData:[image TIFFRepresentation] forType:NSTIFFPboardType];
+
+ // Plain text.
+ } else if ([type isEqualToString:NSStringPboardType]) {
+ DCHECK(!dropData_->plain_text.empty());
+ [pboard setString:SysUTF16ToNSString(dropData_->plain_text)
+ forType:NSStringPboardType];
+
+ // Oops!
+ } else {
+ NOTREACHED() << "Asked for a drag pasteboard type we didn't offer.";
+ }
+}
+
+- (NSPoint)convertScreenPoint:(NSPoint)screenPoint {
+ DCHECK([contentsView_ window]);
+ NSPoint basePoint = [[contentsView_ window] convertScreenToBase:screenPoint];
+ return [contentsView_ convertPoint:basePoint fromView:nil];
+}
+
+- (void)startDrag {
+ NSEvent* currentEvent = [NSApp currentEvent];
+
+ // Synthesize an event for dragging, since we can't be sure that
+ // [NSApp currentEvent] will return a valid dragging event.
+ NSWindow* window = [contentsView_ window];
+ NSPoint position = [window mouseLocationOutsideOfEventStream];
+ NSTimeInterval eventTime = [currentEvent timestamp];
+ NSEvent* dragEvent = [NSEvent mouseEventWithType:NSLeftMouseDragged
+ location:position
+ modifierFlags:NSLeftMouseDraggedMask
+ timestamp:eventTime
+ windowNumber:[window windowNumber]
+ context:nil
+ eventNumber:0
+ clickCount:1
+ pressure:1.0];
+
+ if (dragImage_) {
+ position.x -= imageOffset_.x;
+ // Deal with Cocoa's flipped coordinate system.
+ position.y -= [dragImage_.get() size].height - imageOffset_.y;
+ }
+ // Per kwebster, offset arg is ignored, see -_web_DragImageForElement: in
+ // third_party/WebKit/WebKit/mac/Misc/WebNSViewExtras.m.
+ [window dragImage:[self dragImage]
+ at:position
+ offset:NSZeroSize
+ event:dragEvent
+ pasteboard:pasteboard_
+ source:contentsView_
+ slideBack:YES];
+}
+
+- (void)endDragAt:(NSPoint)screenPoint
+ operation:(NSDragOperation)operation {
+ RenderViewHost* rvh = [contentsView_ tabContents]->render_view_host();
+ if (rvh) {
+ rvh->DragSourceSystemDragEnded();
+
+ // Convert |screenPoint| to view coordinates and flip it.
+ NSPoint localPoint = [self convertScreenPoint:screenPoint];
+ NSRect viewFrame = [contentsView_ frame];
+ localPoint.y = viewFrame.size.height - localPoint.y;
+ // Flip |screenPoint|.
+ NSRect screenFrame = [[[contentsView_ window] screen] frame];
+ screenPoint.y = screenFrame.size.height - screenPoint.y;
+
+ rvh->DragSourceEndedAt(localPoint.x, localPoint.y,
+ screenPoint.x, screenPoint.y,
+ static_cast<WebKit::WebDragOperation>(operation));
+ }
+
+ // Make sure the pasteboard owner isn't us.
+ [pasteboard_ declareTypes:[NSArray array] owner:nil];
+}
+
+- (void)moveDragTo:(NSPoint)screenPoint {
+ RenderViewHost* rvh = [contentsView_ tabContents]->render_view_host();
+ if (rvh) {
+ // Convert |screenPoint| to view coordinates and flip it.
+ NSPoint localPoint = [self convertScreenPoint:screenPoint];
+ NSRect viewFrame = [contentsView_ frame];
+ localPoint.y = viewFrame.size.height - localPoint.y;
+ // Flip |screenPoint|.
+ NSRect screenFrame = [[[contentsView_ window] screen] frame];
+ screenPoint.y = screenFrame.size.height - screenPoint.y;
+
+ rvh->DragSourceMovedTo(localPoint.x, localPoint.y,
+ screenPoint.x, screenPoint.y);
+ }
+}
+
+- (NSString*)dragPromisedFileTo:(NSString*)path {
+ // Be extra paranoid; avoid crashing.
+ if (!dropData_.get()) {
+ NOTREACHED() << "No drag-and-drop data available for promised file.";
+ return nil;
+ }
+
+ FilePath fileName = downloadFileName_.empty() ?
+ GetFileNameFromDragData(*dropData_) : downloadFileName_;
+ FilePath filePath(SysNSStringToUTF8(path));
+ filePath = filePath.Append(fileName);
+ FileStream* fileStream =
+ drag_download_util::CreateFileStreamForDrop(&filePath);
+ if (!fileStream)
+ return nil;
+
+ if (downloadURL_.is_valid()) {
+ TabContents* tabContents = [contentsView_ tabContents];
+ scoped_refptr<DragDownloadFile> dragFileDownloader(new DragDownloadFile(
+ filePath,
+ linked_ptr<net::FileStream>(fileStream),
+ downloadURL_,
+ tabContents->GetURL(),
+ tabContents->encoding(),
+ tabContents));
+
+ // The finalizer will take care of closing and deletion.
+ dragFileDownloader->Start(
+ new drag_download_util::PromiseFileFinalizer(dragFileDownloader));
+ } else {
+ // The writer will take care of closing and deletion.
+ g_browser_process->file_thread()->message_loop()->PostTask(FROM_HERE,
+ new PromiseWriterTask(*dropData_, fileStream));
+ }
+
+ // Once we've created the file, we should return the file name.
+ return SysUTF8ToNSString(filePath.BaseName().value());
+}
+
+@end // @implementation WebDragSource
+
+
+@implementation WebDragSource (Private)
+
+- (void)fillPasteboard {
+ DCHECK(pasteboard_.get());
+
+ [pasteboard_ declareTypes:[NSArray array] owner:contentsView_];
+
+ // HTML.
+ if (!dropData_->text_html.empty())
+ [pasteboard_ addTypes:[NSArray arrayWithObject:NSHTMLPboardType]
+ owner:contentsView_];
+
+ // URL (and title).
+ if (dropData_->url.is_valid())
+ [pasteboard_ addTypes:[NSArray arrayWithObjects:NSURLPboardType,
+ kNSURLTitlePboardType, nil]
+ owner:contentsView_];
+
+ // File.
+ if (!dropData_->file_contents.empty() ||
+ !dropData_->download_metadata.empty()) {
+ NSString* fileExtension = 0;
+
+ if (dropData_->download_metadata.empty()) {
+ // |dropData_->file_extension| comes with the '.', which we must strip.
+ fileExtension = (dropData_->file_extension.length() > 0) ?
+ SysUTF16ToNSString(dropData_->file_extension.substr(1)) : @"";
+ } else {
+ string16 mimeType;
+ FilePath fileName;
+ if (drag_download_util::ParseDownloadMetadata(
+ dropData_->download_metadata,
+ &mimeType,
+ &fileName,
+ &downloadURL_)) {
+ std::string contentDisposition =
+ "attachment; filename=" + fileName.value();
+ download_util::GenerateFileName(downloadURL_,
+ contentDisposition,
+ std::string(),
+ UTF16ToUTF8(mimeType),
+ &downloadFileName_);
+ fileExtension = SysUTF8ToNSString(downloadFileName_.Extension());
+ }
+ }
+
+ if (fileExtension) {
+ // File contents (with and without specific type), file (HFS) promise,
+ // TIFF.
+ // TODO(viettrungluu): others?
+ [pasteboard_ addTypes:[NSArray arrayWithObjects:
+ NSFileContentsPboardType,
+ NSCreateFileContentsPboardType(fileExtension),
+ NSFilesPromisePboardType,
+ NSTIFFPboardType,
+ nil]
+ owner:contentsView_];
+
+ // For the file promise, we need to specify the extension.
+ [pasteboard_ setPropertyList:[NSArray arrayWithObject:fileExtension]
+ forType:NSFilesPromisePboardType];
+ }
+ }
+
+ // Plain text.
+ if (!dropData_->plain_text.empty())
+ [pasteboard_ addTypes:[NSArray arrayWithObject:NSStringPboardType]
+ owner:contentsView_];
+}
+
+- (NSImage*)dragImage {
+ if (dragImage_)
+ return dragImage_;
+
+ // Default to returning a generic image.
+ return nsimage_cache::ImageNamed(@"nav.pdf");
+}
+
+@end // @implementation WebDragSource (Private)
diff --git a/chrome/browser/ui/cocoa/web_drop_target.h b/chrome/browser/ui/cocoa/web_drop_target.h
new file mode 100644
index 0000000..7f18ccf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/web_drop_target.h
@@ -0,0 +1,80 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/string16.h"
+
+class GURL;
+class RenderViewHost;
+class TabContents;
+struct WebDropData;
+
+// A typedef for a RenderViewHost used for comparison purposes only.
+typedef RenderViewHost* RenderViewHostIdentifier;
+
+// A class that handles tracking and event processing for a drag and drop
+// over the content area. Assumes something else initiates the drag, this is
+// only for processing during a drag.
+
+@interface WebDropTarget : NSObject {
+ @private
+ // Our associated TabContents. Weak reference.
+ TabContents* tabContents_;
+
+ // Updated asynchronously during a drag to tell us whether or not we should
+ // allow the drop.
+ NSDragOperation current_operation_;
+
+ // Keep track of the render view host we're dragging over. If it changes
+ // during a drag, we need to re-send the DragEnter message.
+ RenderViewHostIdentifier currentRVH_;
+}
+
+// |contents| is the TabContents representing this tab, used to communicate
+// drag&drop messages to WebCore and handle navigation on a successful drop
+// (if necessary).
+- (id)initWithTabContents:(TabContents*)contents;
+
+// Sets the current operation negotiated by the source and destination,
+// which determines whether or not we should allow the drop. Takes effect the
+// next time |-draggingUpdated:| is called.
+- (void)setCurrentOperation: (NSDragOperation)operation;
+
+// Messages to send during the tracking of a drag, ususally upon receiving
+// calls from the view system. Communicates the drag messages to WebCore.
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info
+ view:(NSView*)view;
+- (void)draggingExited:(id<NSDraggingInfo>)info;
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)info
+ view:(NSView*)view;
+- (BOOL)performDragOperation:(id<NSDraggingInfo>)info
+ view:(NSView*)view;
+
+@end
+
+// Public use only for unit tests.
+@interface WebDropTarget(Testing)
+// Populate the |url| and |title| with URL data in |pboard|. There may be more
+// than one, but we only handle dropping the first. |url| must not be |NULL|;
+// |title| is an optional parameter. Returns |YES| if URL data was obtained from
+// the pasteboard, |NO| otherwise. If |convertFilenames| is |YES|, the function
+// will also attempt to convert filenames in |pboard| to file URLs.
+- (BOOL)populateURL:(GURL*)url
+ andTitle:(string16*)title
+ fromPasteboard:(NSPasteboard*)pboard
+ convertingFilenames:(BOOL)convertFilenames;
+// Given |data|, which should not be nil, fill it in using the contents of the
+// given pasteboard.
+- (void)populateWebDropData:(WebDropData*)data
+ fromPasteboard:(NSPasteboard*)pboard;
+// Given a point in window coordinates and a view in that window, return a
+// flipped point in the coordinate system of |view|.
+- (NSPoint)flipWindowPointToView:(const NSPoint&)windowPoint
+ view:(NSView*)view;
+// Given a point in window coordinates and a view in that window, return a
+// flipped point in screen coordinates.
+- (NSPoint)flipWindowPointToScreen:(const NSPoint&)windowPoint
+ view:(NSView*)view;
+@end
diff --git a/chrome/browser/ui/cocoa/web_drop_target.mm b/chrome/browser/ui/cocoa/web_drop_target.mm
new file mode 100644
index 0000000..08174b5
--- /dev/null
+++ b/chrome/browser/ui/cocoa/web_drop_target.mm
@@ -0,0 +1,283 @@
+// 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/ui/cocoa/web_drop_target.h"
+
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/bookmarks/bookmark_node_data.h"
+#include "chrome/browser/bookmarks/bookmark_pasteboard_helper_mac.h"
+#include "chrome/browser/renderer_host/render_view_host.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#import "third_party/mozilla/NSPasteboard+Utils.h"
+#include "webkit/glue/webdropdata.h"
+#include "webkit/glue/window_open_disposition.h"
+
+using WebKit::WebDragOperationsMask;
+
+@implementation WebDropTarget
+
+// |contents| is the TabContents representing this tab, used to communicate
+// drag&drop messages to WebCore and handle navigation on a successful drop
+// (if necessary).
+- (id)initWithTabContents:(TabContents*)contents {
+ if ((self = [super init])) {
+ tabContents_ = contents;
+ }
+ return self;
+}
+
+// Call to set whether or not we should allow the drop. Takes effect the
+// next time |-draggingUpdated:| is called.
+- (void)setCurrentOperation: (NSDragOperation)operation {
+ current_operation_ = operation;
+}
+
+// Given a point in window coordinates and a view in that window, return a
+// flipped point in the coordinate system of |view|.
+- (NSPoint)flipWindowPointToView:(const NSPoint&)windowPoint
+ view:(NSView*)view {
+ DCHECK(view);
+ NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil];
+ NSRect viewFrame = [view frame];
+ viewPoint.y = viewFrame.size.height - viewPoint.y;
+ return viewPoint;
+}
+
+// Given a point in window coordinates and a view in that window, return a
+// flipped point in screen coordinates.
+- (NSPoint)flipWindowPointToScreen:(const NSPoint&)windowPoint
+ view:(NSView*)view {
+ DCHECK(view);
+ NSPoint screenPoint = [[view window] convertBaseToScreen:windowPoint];
+ NSScreen* screen = [[view window] screen];
+ NSRect screenFrame = [screen frame];
+ screenPoint.y = screenFrame.size.height - screenPoint.y;
+ return screenPoint;
+}
+
+// Return YES if the drop site only allows drops that would navigate. If this
+// is the case, we don't want to pass messages to the renderer because there's
+// really no point (i.e., there's nothing that cares about the mouse position or
+// entering and exiting). One example is an interstitial page (e.g., safe
+// browsing warning).
+- (BOOL)onlyAllowsNavigation {
+ return tabContents_->showing_interstitial_page();
+}
+
+// Messages to send during the tracking of a drag, ususally upon recieving
+// calls from the view system. Communicates the drag messages to WebCore.
+
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info
+ view:(NSView*)view {
+ // Save off the RVH so we can tell if it changes during a drag. If it does,
+ // we need to send a new enter message in draggingUpdated:.
+ currentRVH_ = tabContents_->render_view_host();
+
+ if ([self onlyAllowsNavigation]) {
+ if ([[info draggingPasteboard] containsURLData])
+ return NSDragOperationCopy;
+ return NSDragOperationNone;
+ }
+
+ // If the tab is showing the boomark manager, send BookmarkDrag events
+ RenderViewHostDelegate::BookmarkDrag* dragDelegate =
+ tabContents_->GetBookmarkDragDelegate();
+ BookmarkNodeData dragData;
+ if(dragDelegate && dragData.ReadFromDragClipboard())
+ dragDelegate->OnDragEnter(dragData);
+
+ // Fill out a WebDropData from pasteboard.
+ WebDropData data;
+ [self populateWebDropData:&data fromPasteboard:[info draggingPasteboard]];
+
+ // Create the appropriate mouse locations for WebCore. The draggingLocation
+ // is in window coordinates. Both need to be flipped.
+ NSPoint windowPoint = [info draggingLocation];
+ NSPoint viewPoint = [self flipWindowPointToView:windowPoint view:view];
+ NSPoint screenPoint = [self flipWindowPointToScreen:windowPoint view:view];
+ NSDragOperation mask = [info draggingSourceOperationMask];
+ tabContents_->render_view_host()->DragTargetDragEnter(data,
+ gfx::Point(viewPoint.x, viewPoint.y),
+ gfx::Point(screenPoint.x, screenPoint.y),
+ static_cast<WebDragOperationsMask>(mask));
+
+ // We won't know the true operation (whether the drag is allowed) until we
+ // hear back from the renderer. For now, be optimistic:
+ current_operation_ = NSDragOperationCopy;
+ return current_operation_;
+}
+
+- (void)draggingExited:(id<NSDraggingInfo>)info {
+ DCHECK(currentRVH_);
+ if (currentRVH_ != tabContents_->render_view_host())
+ return;
+
+ // Nothing to do in the interstitial case.
+
+ tabContents_->render_view_host()->DragTargetDragLeave();
+}
+
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)info
+ view:(NSView*)view {
+ DCHECK(currentRVH_);
+ if (currentRVH_ != tabContents_->render_view_host())
+ [self draggingEntered:info view:view];
+
+ if ([self onlyAllowsNavigation]) {
+ if ([[info draggingPasteboard] containsURLData])
+ return NSDragOperationCopy;
+ return NSDragOperationNone;
+ }
+
+ // Create the appropriate mouse locations for WebCore. The draggingLocation
+ // is in window coordinates.
+ NSPoint windowPoint = [info draggingLocation];
+ NSPoint viewPoint = [self flipWindowPointToView:windowPoint view:view];
+ NSPoint screenPoint = [self flipWindowPointToScreen:windowPoint view:view];
+ NSDragOperation mask = [info draggingSourceOperationMask];
+ tabContents_->render_view_host()->DragTargetDragOver(
+ gfx::Point(viewPoint.x, viewPoint.y),
+ gfx::Point(screenPoint.x, screenPoint.y),
+ static_cast<WebDragOperationsMask>(mask));
+
+ // If the tab is showing the boomark manager, send BookmarkDrag events
+ RenderViewHostDelegate::BookmarkDrag* dragDelegate =
+ tabContents_->GetBookmarkDragDelegate();
+ BookmarkNodeData dragData;
+ if(dragDelegate && dragData.ReadFromDragClipboard())
+ dragDelegate->OnDragOver(dragData);
+ return current_operation_;
+}
+
+- (BOOL)performDragOperation:(id<NSDraggingInfo>)info
+ view:(NSView*)view {
+ if (currentRVH_ != tabContents_->render_view_host())
+ [self draggingEntered:info view:view];
+
+ // Check if we only allow navigation and navigate to a url on the pasteboard.
+ if ([self onlyAllowsNavigation]) {
+ NSPasteboard* pboard = [info draggingPasteboard];
+ if ([pboard containsURLData]) {
+ GURL url;
+ [self populateURL:&url
+ andTitle:NULL
+ fromPasteboard:pboard
+ convertingFilenames:YES];
+ tabContents_->OpenURL(url, GURL(), CURRENT_TAB,
+ PageTransition::AUTO_BOOKMARK);
+ return YES;
+ }
+ return NO;
+ }
+
+ // If the tab is showing the boomark manager, send BookmarkDrag events
+ RenderViewHostDelegate::BookmarkDrag* dragDelegate =
+ tabContents_->GetBookmarkDragDelegate();
+ BookmarkNodeData dragData;
+ if(dragDelegate && dragData.ReadFromDragClipboard())
+ dragDelegate->OnDrop(dragData);
+
+ currentRVH_ = NULL;
+
+ // Create the appropriate mouse locations for WebCore. The draggingLocation
+ // is in window coordinates. Both need to be flipped.
+ NSPoint windowPoint = [info draggingLocation];
+ NSPoint viewPoint = [self flipWindowPointToView:windowPoint view:view];
+ NSPoint screenPoint = [self flipWindowPointToScreen:windowPoint view:view];
+ tabContents_->render_view_host()->DragTargetDrop(
+ gfx::Point(viewPoint.x, viewPoint.y),
+ gfx::Point(screenPoint.x, screenPoint.y));
+
+ return YES;
+}
+
+// Populate the |url| and |title| with URL data in |pboard|. There may be more
+// than one, but we only handle dropping the first. |url| must not be |NULL|;
+// |title| is an optional parameter. Returns |YES| if URL data was obtained from
+// the pasteboard, |NO| otherwise. If |convertFilenames| is |YES|, the function
+// will also attempt to convert filenames in |pboard| to file URLs.
+- (BOOL)populateURL:(GURL*)url
+ andTitle:(string16*)title
+ fromPasteboard:(NSPasteboard*)pboard
+ convertingFilenames:(BOOL)convertFilenames {
+ DCHECK(url);
+ DCHECK(title);
+
+ // Bail out early if there's no URL data.
+ if (![pboard containsURLData])
+ return NO;
+
+ // |-getURLs:andTitles:convertingFilenames:| will already validate URIs so we
+ // don't need to again. The arrays returned are both of NSString's.
+ NSArray* urls = nil;
+ NSArray* titles = nil;
+ [pboard getURLs:&urls andTitles:&titles convertingFilenames:convertFilenames];
+ DCHECK_EQ([urls count], [titles count]);
+ // It's possible that no URLs were actually provided!
+ if (![urls count])
+ return NO;
+ NSString* urlString = [urls objectAtIndex:0];
+ if ([urlString length]) {
+ // Check again just to make sure to not assign NULL into a std::string,
+ // which throws an exception.
+ const char* utf8Url = [urlString UTF8String];
+ if (utf8Url) {
+ *url = GURL(utf8Url);
+ // Extra paranoia check.
+ if (title && [titles count])
+ *title = base::SysNSStringToUTF16([titles objectAtIndex:0]);
+ }
+ }
+ return YES;
+}
+
+// Given |data|, which should not be nil, fill it in using the contents of the
+// given pasteboard.
+- (void)populateWebDropData:(WebDropData*)data
+ fromPasteboard:(NSPasteboard*)pboard {
+ DCHECK(data);
+ DCHECK(pboard);
+ NSArray* types = [pboard types];
+
+ // Get URL if possible. To avoid exposing file system paths to web content,
+ // filenames in the drag are not converted to file URLs.
+ [self populateURL:&data->url
+ andTitle:&data->url_title
+ fromPasteboard:pboard
+ convertingFilenames:NO];
+
+ // Get plain text.
+ if ([types containsObject:NSStringPboardType]) {
+ data->plain_text =
+ base::SysNSStringToUTF16([pboard stringForType:NSStringPboardType]);
+ }
+
+ // Get HTML. If there's no HTML, try RTF.
+ if ([types containsObject:NSHTMLPboardType]) {
+ data->text_html =
+ base::SysNSStringToUTF16([pboard stringForType:NSHTMLPboardType]);
+ } else if ([types containsObject:NSRTFPboardType]) {
+ NSString* html = [pboard htmlFromRtf];
+ data->text_html = base::SysNSStringToUTF16(html);
+ }
+
+ // Get files.
+ if ([types containsObject:NSFilenamesPboardType]) {
+ NSArray* files = [pboard propertyListForType:NSFilenamesPboardType];
+ if ([files isKindOfClass:[NSArray class]] && [files count]) {
+ for (NSUInteger i = 0; i < [files count]; i++) {
+ NSString* filename = [files objectAtIndex:i];
+ BOOL isDir = NO;
+ BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:filename
+ isDirectory:&isDir];
+ if (exists && !isDir)
+ data->filenames.push_back(base::SysNSStringToUTF16(filename));
+ }
+ }
+ }
+
+ // TODO(pinkerton): Get file contents. http://crbug.com/34661
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/web_drop_target_unittest.mm b/chrome/browser/ui/cocoa/web_drop_target_unittest.mm
new file mode 100644
index 0000000..0261e89
--- /dev/null
+++ b/chrome/browser/ui/cocoa/web_drop_target_unittest.mm
@@ -0,0 +1,166 @@
+// 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/mac/scoped_nsautorelease_pool.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/renderer_host/test/test_render_view_host.h"
+#include "chrome/browser/tab_contents/test_tab_contents.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/web_drop_target.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "third_party/mozilla/NSPasteboard+Utils.h"
+#include "webkit/glue/webdropdata.h"
+
+class WebDropTargetTest : public RenderViewHostTestHarness {
+ public:
+ virtual void SetUp() {
+ RenderViewHostTestHarness::SetUp();
+ CocoaTest::BootstrapCocoa();
+ drop_target_.reset([[WebDropTarget alloc] initWithTabContents:contents()]);
+ }
+
+ void PutURLOnPasteboard(NSString* urlString, NSPasteboard* pboard) {
+ [pboard declareTypes:[NSArray arrayWithObject:NSURLPboardType]
+ owner:nil];
+ NSURL* url = [NSURL URLWithString:urlString];
+ EXPECT_TRUE(url);
+ [url writeToPasteboard:pboard];
+ }
+
+ void PutCoreURLAndTitleOnPasteboard(NSString* urlString, NSString* title,
+ NSPasteboard* pboard) {
+ [pboard
+ declareTypes:[NSArray arrayWithObjects:kCorePasteboardFlavorType_url,
+ kCorePasteboardFlavorType_urln,
+ nil]
+ owner:nil];
+ [pboard setString:urlString
+ forType:kCorePasteboardFlavorType_url];
+ [pboard setString:title
+ forType:kCorePasteboardFlavorType_urln];
+ }
+
+ base::mac::ScopedNSAutoreleasePool pool_;
+ scoped_nsobject<WebDropTarget> drop_target_;
+};
+
+// Make sure nothing leaks.
+TEST_F(WebDropTargetTest, Init) {
+ EXPECT_TRUE(drop_target_);
+}
+
+// Test flipping of coordinates given a point in window coordinates.
+TEST_F(WebDropTargetTest, Flip) {
+ NSPoint windowPoint = NSZeroPoint;
+ scoped_nsobject<NSWindow> window([[CocoaTestHelperWindow alloc] init]);
+ NSPoint viewPoint =
+ [drop_target_ flipWindowPointToView:windowPoint
+ view:[window contentView]];
+ NSPoint screenPoint =
+ [drop_target_ flipWindowPointToScreen:windowPoint
+ view:[window contentView]];
+ EXPECT_EQ(0, viewPoint.x);
+ EXPECT_EQ(600, viewPoint.y);
+ EXPECT_EQ(0, screenPoint.x);
+ // We can't put a value on the screen size since everyone will have a
+ // different one.
+ EXPECT_NE(0, screenPoint.y);
+}
+
+TEST_F(WebDropTargetTest, URL) {
+ NSPasteboard* pboard = nil;
+ NSString* url = nil;
+ NSString* title = nil;
+ GURL result_url;
+ string16 result_title;
+
+ // Put a URL on the pasteboard and check it.
+ pboard = [NSPasteboard pasteboardWithUniqueName];
+ url = @"http://www.google.com/";
+ PutURLOnPasteboard(url, pboard);
+ EXPECT_TRUE([drop_target_ populateURL:&result_url
+ andTitle:&result_title
+ fromPasteboard:pboard
+ convertingFilenames:NO]);
+ EXPECT_EQ(base::SysNSStringToUTF8(url), result_url.spec());
+ [pboard releaseGlobally];
+
+ // Put a 'url ' and 'urln' on the pasteboard and check it.
+ pboard = [NSPasteboard pasteboardWithUniqueName];
+ url = @"http://www.google.com/";
+ title = @"Title of Awesomeness!",
+ PutCoreURLAndTitleOnPasteboard(url, title, pboard);
+ EXPECT_TRUE([drop_target_ populateURL:&result_url
+ andTitle:&result_title
+ fromPasteboard:pboard
+ convertingFilenames:NO]);
+ EXPECT_EQ(base::SysNSStringToUTF8(url), result_url.spec());
+ EXPECT_EQ(base::SysNSStringToUTF16(title), result_title);
+ [pboard releaseGlobally];
+
+ // Also check that it passes file:// via 'url '/'urln' properly.
+ pboard = [NSPasteboard pasteboardWithUniqueName];
+ url = @"file:///tmp/dont_delete_me.txt";
+ title = @"very important";
+ PutCoreURLAndTitleOnPasteboard(url, title, pboard);
+ EXPECT_TRUE([drop_target_ populateURL:&result_url
+ andTitle:&result_title
+ fromPasteboard:pboard
+ convertingFilenames:NO]);
+ EXPECT_EQ(base::SysNSStringToUTF8(url), result_url.spec());
+ EXPECT_EQ(base::SysNSStringToUTF16(title), result_title);
+ [pboard releaseGlobally];
+
+ // And javascript:.
+ pboard = [NSPasteboard pasteboardWithUniqueName];
+ url = @"javascript:open('http://www.youtube.com/')";
+ title = @"kill some time";
+ PutCoreURLAndTitleOnPasteboard(url, title, pboard);
+ EXPECT_TRUE([drop_target_ populateURL:&result_url
+ andTitle:&result_title
+ fromPasteboard:pboard
+ convertingFilenames:NO]);
+ EXPECT_EQ(base::SysNSStringToUTF8(url), result_url.spec());
+ EXPECT_EQ(base::SysNSStringToUTF16(title), result_title);
+ [pboard releaseGlobally];
+
+ pboard = [NSPasteboard pasteboardWithUniqueName];
+ url = @"/bin/sh";
+ [pboard declareTypes:[NSArray arrayWithObject:NSFilenamesPboardType]
+ owner:nil];
+ [pboard setPropertyList:[NSArray arrayWithObject:url]
+ forType:NSFilenamesPboardType];
+ EXPECT_FALSE([drop_target_ populateURL:&result_url
+ andTitle:&result_title
+ fromPasteboard:pboard
+ convertingFilenames:NO]);
+ EXPECT_TRUE([drop_target_ populateURL:&result_url
+ andTitle:&result_title
+ fromPasteboard:pboard
+ convertingFilenames:YES]);
+ EXPECT_EQ("file://localhost/bin/sh", result_url.spec());
+ EXPECT_EQ("sh", UTF16ToUTF8(result_title));
+ [pboard releaseGlobally];
+}
+
+TEST_F(WebDropTargetTest, Data) {
+ WebDropData data;
+ NSPasteboard* pboard = [NSPasteboard pasteboardWithUniqueName];
+
+ PutURLOnPasteboard(@"http://www.google.com", pboard);
+ [pboard addTypes:[NSArray arrayWithObjects:NSHTMLPboardType,
+ NSStringPboardType, nil]
+ owner:nil];
+ NSString* htmlString = @"<html><body><b>hi there</b></body></html>";
+ NSString* textString = @"hi there";
+ [pboard setString:htmlString forType:NSHTMLPboardType];
+ [pboard setString:textString forType:NSStringPboardType];
+ [drop_target_ populateWebDropData:&data fromPasteboard:pboard];
+ EXPECT_EQ(data.url.spec(), "http://www.google.com/");
+ EXPECT_EQ(base::SysNSStringToUTF16(textString), data.plain_text);
+ EXPECT_EQ(base::SysNSStringToUTF16(htmlString), data.text_html);
+
+ [pboard releaseGlobally];
+}
diff --git a/chrome/browser/ui/cocoa/window_size_autosaver.h b/chrome/browser/ui/cocoa/window_size_autosaver.h
new file mode 100644
index 0000000..5dbcff4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/window_size_autosaver.h
@@ -0,0 +1,35 @@
+// 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_UI_COCOA_WINDOW_SIZE_AUTOSAVER_H_
+#define CHROME_BROWSER_UI_COCOA_WINDOW_SIZE_AUTOSAVER_H_
+#pragma once
+
+class PrefService;
+@class NSWindow;
+
+// WindowSizeAutosaver is a helper class that makes it easy to let windows
+// autoremember their position or position and size in a PrefService object.
+// To use this, add a |scoped_nsobject<WindowSizeAutosaver>| to your window
+// controller and initialize it in the window controller's init method, passing
+// a window and an autosave name. The autosaver will register for "window moved"
+// and "window resized" notifications and write the current window state to the
+// prefs service every time they fire. The window's size is automatically
+// restored when the autosaver's |initWithWindow:...| method is called.
+//
+// Note: Your xib file should have "Visible at launch" UNCHECKED, so that the
+// initial repositioning is not visible.
+@interface WindowSizeAutosaver : NSObject {
+ NSWindow* window_; // weak
+ PrefService* prefService_; // weak
+ const char* path_;
+}
+
+- (id)initWithWindow:(NSWindow*)window
+ prefService:(PrefService*)prefs
+ path:(const char*)path;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_WINDOW_SIZE_AUTOSAVER_H_
+
diff --git a/chrome/browser/ui/cocoa/window_size_autosaver.mm b/chrome/browser/ui/cocoa/window_size_autosaver.mm
new file mode 100644
index 0000000..5ca9878
--- /dev/null
+++ b/chrome/browser/ui/cocoa/window_size_autosaver.mm
@@ -0,0 +1,108 @@
+// 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/ui/cocoa/window_size_autosaver.h"
+
+#include "chrome/browser/prefs/pref_service.h"
+
+// If the window width stored in the prefs is smaller than this, the size is
+// not restored but instead cleared from the profile -- to protect users from
+// accidentally making their windows very small and then not finding them again.
+const int kMinWindowWidth = 101;
+
+// Minimum restored window height, see |kMinWindowWidth|.
+const int kMinWindowHeight = 17;
+
+@interface WindowSizeAutosaver (Private)
+- (void)save:(NSNotification*)notification;
+- (void)restore;
+@end
+
+@implementation WindowSizeAutosaver
+
+- (id)initWithWindow:(NSWindow*)window
+ prefService:(PrefService*)prefs
+ path:(const char*)path {
+ if ((self = [super init])) {
+ window_ = window;
+ prefService_ = prefs;
+ path_ = path;
+
+ [self restore];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(save:)
+ name:NSWindowDidMoveNotification
+ object:window_];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(save:)
+ name:NSWindowDidResizeNotification
+ object:window_];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+- (void)save:(NSNotification*)notification {
+ DictionaryValue* windowPrefs = prefService_->GetMutableDictionary(path_);
+ NSRect frame = [window_ frame];
+ if ([window_ styleMask] & NSResizableWindowMask) {
+ // Save the origin of the window.
+ windowPrefs->SetInteger("left", NSMinX(frame));
+ windowPrefs->SetInteger("right", NSMaxX(frame));
+ // windows's and linux's profiles have top < bottom due to having their
+ // screen origin in the upper left, while cocoa's is in the lower left. To
+ // keep the top < bottom invariant, store top in bottom and vice versa.
+ windowPrefs->SetInteger("top", NSMinY(frame));
+ windowPrefs->SetInteger("bottom", NSMaxY(frame));
+ } else {
+ // Save the origin of the window.
+ windowPrefs->SetInteger("x", frame.origin.x);
+ windowPrefs->SetInteger("y", frame.origin.y);
+ }
+}
+
+- (void)restore {
+ // Get the positioning information.
+ DictionaryValue* windowPrefs = prefService_->GetMutableDictionary(path_);
+ if ([window_ styleMask] & NSResizableWindowMask) {
+ int x1, x2, y1, y2;
+ if (!windowPrefs->GetInteger("left", &x1) ||
+ !windowPrefs->GetInteger("right", &x2) ||
+ !windowPrefs->GetInteger("top", &y1) ||
+ !windowPrefs->GetInteger("bottom", &y2)) {
+ return;
+ }
+ if (x2 - x1 < kMinWindowWidth || y2 - y1 < kMinWindowHeight) {
+ // Windows should never be very small.
+ windowPrefs->Remove("left", NULL);
+ windowPrefs->Remove("right", NULL);
+ windowPrefs->Remove("top", NULL);
+ windowPrefs->Remove("bottom", NULL);
+ } else {
+ [window_ setFrame:NSMakeRect(x1, y1, x2 - x1, y2 - y1) display:YES];
+
+ // Make sure the window is on-screen.
+ [window_ cascadeTopLeftFromPoint:NSZeroPoint];
+ }
+ } else {
+ int x, y;
+ if (!windowPrefs->GetInteger("x", &x) ||
+ !windowPrefs->GetInteger("y", &y))
+ return; // Nothing stored.
+ // Turn the origin (lower-left) into an upper-left window point.
+ NSPoint upperLeft = NSMakePoint(x, y + NSHeight([window_ frame]));
+ [window_ cascadeTopLeftFromPoint:upperLeft];
+ }
+}
+
+@end
+
diff --git a/chrome/browser/ui/cocoa/window_size_autosaver_unittest.mm b/chrome/browser/ui/cocoa/window_size_autosaver_unittest.mm
new file mode 100644
index 0000000..c3b33ed
--- /dev/null
+++ b/chrome/browser/ui/cocoa/window_size_autosaver_unittest.mm
@@ -0,0 +1,201 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/window_size_autosaver.h"
+
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class WindowSizeAutosaverTest : public CocoaTest {
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ path_ = "WindowSizeAutosaverTest";
+ window_ =
+ [[NSWindow alloc] initWithContentRect:NSMakeRect(100, 101, 150, 151)
+ styleMask:NSTitledWindowMask|
+ NSResizableWindowMask
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ browser_helper_.profile()->GetPrefs()->RegisterDictionaryPref(path_);
+ }
+
+ virtual void TearDown() {
+ [window_ close];
+ CocoaTest::TearDown();
+ }
+
+ public:
+ BrowserTestHelper browser_helper_;
+ NSWindow* window_;
+ const char* path_;
+};
+
+TEST_F(WindowSizeAutosaverTest, RestoresAndSavesPos) {
+ PrefService* pref = browser_helper_.profile()->GetPrefs();
+ ASSERT_TRUE(pref != NULL);
+
+ // Check to make sure there is no existing pref for window placement.
+ ASSERT_TRUE(pref->GetDictionary(path_) == NULL);
+
+ // Replace the window with one that doesn't have resize controls.
+ [window_ close];
+ window_ =
+ [[NSWindow alloc] initWithContentRect:NSMakeRect(100, 101, 150, 151)
+ styleMask:NSTitledWindowMask
+ backing:NSBackingStoreBuffered
+ defer:NO];
+
+ // Ask the window to save its position, then check that a preference
+ // exists. We're technically passing in a pointer to the user prefs
+ // and not the local state prefs, but a PrefService* is a
+ // PrefService*, and this is a unittest.
+
+ {
+ NSRect frame = [window_ frame];
+ // Empty state, shouldn't restore:
+ scoped_nsobject<WindowSizeAutosaver> sizeSaver([[WindowSizeAutosaver alloc]
+ initWithWindow:window_
+ prefService:pref
+ path:path_]);
+ EXPECT_EQ(NSMinX(frame), NSMinX([window_ frame]));
+ EXPECT_EQ(NSMinY(frame), NSMinY([window_ frame]));
+ EXPECT_EQ(NSWidth(frame), NSWidth([window_ frame]));
+ EXPECT_EQ(NSHeight(frame), NSHeight([window_ frame]));
+
+ // Move and resize window, should store position but not size.
+ [window_ setFrame:NSMakeRect(300, 310, 250, 252) display:NO];
+ }
+
+ // Another window movement -- shouldn't be recorded.
+ [window_ setFrame:NSMakeRect(400, 420, 160, 162) display:NO];
+
+ {
+ // Should restore last stored position, but not size.
+ scoped_nsobject<WindowSizeAutosaver> sizeSaver([[WindowSizeAutosaver alloc]
+ initWithWindow:window_
+ prefService:pref
+ path:path_]);
+ EXPECT_EQ(300, NSMinX([window_ frame]));
+ EXPECT_EQ(310, NSMinY([window_ frame]));
+ EXPECT_EQ(160, NSWidth([window_ frame]));
+ EXPECT_EQ(162, NSHeight([window_ frame]));
+ }
+
+ // ...and it should be in the profile, too.
+ EXPECT_TRUE(pref->GetDictionary(path_) != NULL);
+ int x, y;
+ DictionaryValue* windowPref = pref->GetMutableDictionary(path_);
+ EXPECT_FALSE(windowPref->GetInteger("left", &x));
+ EXPECT_FALSE(windowPref->GetInteger("right", &x));
+ EXPECT_FALSE(windowPref->GetInteger("top", &x));
+ EXPECT_FALSE(windowPref->GetInteger("bottom", &x));
+ ASSERT_TRUE(windowPref->GetInteger("x", &x));
+ ASSERT_TRUE(windowPref->GetInteger("y", &y));
+ EXPECT_EQ(300, x);
+ EXPECT_EQ(310, y);
+}
+
+TEST_F(WindowSizeAutosaverTest, RestoresAndSavesRect) {
+ PrefService* pref = browser_helper_.profile()->GetPrefs();
+ ASSERT_TRUE(pref != NULL);
+
+ // Check to make sure there is no existing pref for window placement.
+ ASSERT_TRUE(pref->GetDictionary(path_) == NULL);
+
+ // Ask the window to save its position, then check that a preference
+ // exists. We're technically passing in a pointer to the user prefs
+ // and not the local state prefs, but a PrefService* is a
+ // PrefService*, and this is a unittest.
+
+ {
+ NSRect frame = [window_ frame];
+ // Empty state, shouldn't restore:
+ scoped_nsobject<WindowSizeAutosaver> sizeSaver([[WindowSizeAutosaver alloc]
+ initWithWindow:window_
+ prefService:pref
+ path:path_]);
+ EXPECT_EQ(NSMinX(frame), NSMinX([window_ frame]));
+ EXPECT_EQ(NSMinY(frame), NSMinY([window_ frame]));
+ EXPECT_EQ(NSWidth(frame), NSWidth([window_ frame]));
+ EXPECT_EQ(NSHeight(frame), NSHeight([window_ frame]));
+
+ // Move and resize window, should store
+ [window_ setFrame:NSMakeRect(300, 310, 250, 252) display:NO];
+ }
+
+ // Another window movement -- shouldn't be recorded.
+ [window_ setFrame:NSMakeRect(400, 420, 160, 162) display:NO];
+
+ {
+ // Should restore last stored size
+ scoped_nsobject<WindowSizeAutosaver> sizeSaver([[WindowSizeAutosaver alloc]
+ initWithWindow:window_
+ prefService:pref
+ path:path_]);
+ EXPECT_EQ(300, NSMinX([window_ frame]));
+ EXPECT_EQ(310, NSMinY([window_ frame]));
+ EXPECT_EQ(250, NSWidth([window_ frame]));
+ EXPECT_EQ(252, NSHeight([window_ frame]));
+ }
+
+ // ...and it should be in the profile, too.
+ EXPECT_TRUE(pref->GetDictionary(path_) != NULL);
+ int x1, y1, x2, y2;
+ DictionaryValue* windowPref = pref->GetMutableDictionary(path_);
+ EXPECT_FALSE(windowPref->GetInteger("x", &x1));
+ EXPECT_FALSE(windowPref->GetInteger("y", &x1));
+ ASSERT_TRUE(windowPref->GetInteger("left", &x1));
+ ASSERT_TRUE(windowPref->GetInteger("right", &x2));
+ ASSERT_TRUE(windowPref->GetInteger("top", &y1));
+ ASSERT_TRUE(windowPref->GetInteger("bottom", &y2));
+ EXPECT_EQ(300, x1);
+ EXPECT_EQ(310, y1);
+ EXPECT_EQ(300 + 250, x2);
+ EXPECT_EQ(310 + 252, y2);
+}
+
+// http://crbug.com/39625
+TEST_F(WindowSizeAutosaverTest, DoesNotRestoreButClearsEmptyRect) {
+ PrefService* pref = browser_helper_.profile()->GetPrefs();
+ ASSERT_TRUE(pref != NULL);
+
+ DictionaryValue* windowPref = pref->GetMutableDictionary(path_);
+ windowPref->SetInteger("left", 50);
+ windowPref->SetInteger("right", 50);
+ windowPref->SetInteger("top", 60);
+ windowPref->SetInteger("bottom", 60);
+
+ {
+ // Window rect shouldn't change...
+ NSRect frame = [window_ frame];
+ scoped_nsobject<WindowSizeAutosaver> sizeSaver([[WindowSizeAutosaver alloc]
+ initWithWindow:window_
+ prefService:pref
+ path:path_]);
+ EXPECT_EQ(NSMinX(frame), NSMinX([window_ frame]));
+ EXPECT_EQ(NSMinY(frame), NSMinY([window_ frame]));
+ EXPECT_EQ(NSWidth(frame), NSWidth([window_ frame]));
+ EXPECT_EQ(NSHeight(frame), NSHeight([window_ frame]));
+ }
+
+ // ...and it should be gone from the profile, too.
+ EXPECT_TRUE(pref->GetDictionary(path_) != NULL);
+ int x1, y1, x2, y2;
+ EXPECT_FALSE(windowPref->GetInteger("x", &x1));
+ EXPECT_FALSE(windowPref->GetInteger("y", &x1));
+ ASSERT_FALSE(windowPref->GetInteger("left", &x1));
+ ASSERT_FALSE(windowPref->GetInteger("right", &x2));
+ ASSERT_FALSE(windowPref->GetInteger("top", &y1));
+ ASSERT_FALSE(windowPref->GetInteger("bottom", &y2));
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/wrench_menu_button_cell.h b/chrome/browser/ui/cocoa/wrench_menu_button_cell.h
new file mode 100644
index 0000000..c2d3432
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu_button_cell.h
@@ -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.
+
+#ifndef CHROME_BROWSER_UI_COCOA_WRENCH_MENU_BUTTON_CELL_H_
+#define CHROME_BROWSER_UI_COCOA_WRENCH_MENU_BUTTON_CELL_H_
+
+#import <Cocoa/Cocoa.h>
+
+// The WrenchMenuButtonCell overrides drawing the background gradient to use
+// the same colors as NSSmallSquareBezelStyle but as a smooth gradient, rather
+// than two blocks of colors. This also uses the blue menu highlight color for
+// the pressed state.
+@interface WrenchMenuButtonCell : NSButtonCell {
+}
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_WRENCH_MENU_BUTTON_CELL_H_
diff --git a/chrome/browser/ui/cocoa/wrench_menu_button_cell.mm b/chrome/browser/ui/cocoa/wrench_menu_button_cell.mm
new file mode 100644
index 0000000..c2a15b7
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu_button_cell.mm
@@ -0,0 +1,48 @@
+// 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/ui/cocoa/wrench_menu_button_cell.h"
+
+#include "base/scoped_nsobject.h"
+
+@implementation WrenchMenuButtonCell
+
+- (void)drawBezelWithFrame:(NSRect)frame inView:(NSView*)controlView {
+ [NSGraphicsContext saveGraphicsState];
+
+ // Inset the rect to match the appearance of the layout of interface builder.
+ // The bounding rect of buttons is actually larger than the display rect shown
+ // there.
+ frame = NSInsetRect(frame, 0.0, 1.0);
+
+ // Stroking the rect gives a weak stroke. Filling and insetting gives a
+ // strong, un-anti-aliased border.
+ [[NSColor colorWithDeviceWhite:0.663 alpha:1.0] set];
+ NSRectFill(frame);
+ frame = NSInsetRect(frame, 1.0, 1.0);
+
+ // The default state should be a subtle gray gradient.
+ if (![self isHighlighted]) {
+ NSColor* end = [NSColor colorWithDeviceWhite:0.922 alpha:1.0];
+ scoped_nsobject<NSGradient> gradient(
+ [[NSGradient alloc] initWithStartingColor:[NSColor whiteColor]
+ endingColor:end]);
+ [gradient drawInRect:frame angle:90.0];
+ } else {
+ // |+selectedMenuItemColor| appears to be a gradient, so just filling the
+ // rect with that color produces the desired effect.
+ [[NSColor selectedMenuItemColor] set];
+ NSRectFill(frame);
+ }
+
+ [NSGraphicsContext restoreGraphicsState];
+}
+
+- (NSBackgroundStyle)interiorBackgroundStyle {
+ if ([self isHighlighted])
+ return NSBackgroundStyleDark;
+ return [super interiorBackgroundStyle];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/wrench_menu_button_cell_unittest.mm b/chrome/browser/ui/cocoa/wrench_menu_button_cell_unittest.mm
new file mode 100644
index 0000000..7ab1588
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu_button_cell_unittest.mm
@@ -0,0 +1,51 @@
+// 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"
+#include "chrome/app/chrome_command_ids.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/wrench_menu_button_cell.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface TestWrenchMenuButton : NSButton
+@end
+@implementation TestWrenchMenuButton
++ (Class)cellClass {
+ return [WrenchMenuButtonCell class];
+}
+@end
+
+namespace {
+
+class WrenchMenuButtonCellTest : public CocoaTest {
+ public:
+ void SetUp() {
+ CocoaTest::SetUp();
+
+ NSRect frame = NSMakeRect(10, 10, 50, 19);
+ button_.reset([[TestWrenchMenuButton alloc] initWithFrame:frame]);
+ [button_ setBezelStyle:NSSmallSquareBezelStyle];
+ [[button_ cell] setControlSize:NSSmallControlSize];
+ [button_ setTitle:@"Allays"];
+ [button_ setButtonType:NSMomentaryPushInButton];
+ }
+
+ scoped_nsobject<NSButton> button_;
+};
+
+TEST_F(WrenchMenuButtonCellTest, Draw) {
+ ASSERT_TRUE(button_.get());
+ [[test_window() contentView] addSubview:button_.get()];
+ [button_ setNeedsDisplay:YES];
+}
+
+TEST_F(WrenchMenuButtonCellTest, DrawHighlight) {
+ ASSERT_TRUE(button_.get());
+ [[test_window() contentView] addSubview:button_.get()];
+ [button_ highlight:YES];
+ [button_ setNeedsDisplay:YES];
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/wrench_menu_controller.h b/chrome/browser/ui/cocoa/wrench_menu_controller.h
new file mode 100644
index 0000000..5f1d9ca
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu_controller.h
@@ -0,0 +1,72 @@
+// 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_UI_COCOA_WRENCH_MENU_CONTROLLER_H_
+#define CHROME_BROWSER_UI_COCOA_WRENCH_MENU_CONTROLLER_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/cocoa_protocols_mac.h"
+#include "base/scoped_ptr.h"
+#import "chrome/browser/ui/cocoa/menu_controller.h"
+
+@class MenuTrackedRootView;
+@class ToolbarController;
+class WrenchMenuModel;
+
+namespace WrenchMenuControllerInternal {
+class ZoomLevelObserver;
+} // namespace WrenchMenuControllerInternal
+
+// The Wrench menu has a creative layout, with buttons in menu items. There is
+// a cross-platform model for this special menu, but on the Mac it's easier to
+// get spacing and alignment precisely right using a NIB. To do that, we
+// subclass the generic MenuController implementation and special-case the two
+// items that require specific layout and load them from the NIB.
+//
+// This object is instantiated in Toolbar.xib and is configured by the
+// ToolbarController.
+@interface WrenchMenuController : MenuController<NSMenuDelegate> {
+ IBOutlet MenuTrackedRootView* editItem_;
+ IBOutlet NSButton* editCut_;
+ IBOutlet NSButton* editCopy_;
+ IBOutlet NSButton* editPaste_;
+
+ IBOutlet MenuTrackedRootView* zoomItem_;
+ IBOutlet NSButton* zoomPlus_;
+ IBOutlet NSButton* zoomDisplay_;
+ IBOutlet NSButton* zoomMinus_;
+ IBOutlet NSButton* zoomFullScreen_;
+
+ scoped_ptr<WrenchMenuControllerInternal::ZoomLevelObserver> observer_;
+}
+
+// Designated initializer; called within the NIB.
+- (id)init;
+
+// Used to dispatch commands from the Wrench menu. The custom items within the
+// menu cannot be hooked up directly to First Responder because the window in
+// which the controls reside is not the BrowserWindowController, but a
+// NSCarbonMenuWindow; this screws up the typical |-commandDispatch:| system.
+- (IBAction)dispatchWrenchMenuCommand:(id)sender;
+
+// Returns the weak reference to the WrenchMenuModel.
+- (WrenchMenuModel*)wrenchMenuModel;
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////
+
+@interface WrenchMenuController (UnitTesting)
+// |-dispatchWrenchMenuCommand:| calls this after it has determined the tag of
+// the sender. The default implementation executes the command on the outermost
+// run loop using |-performSelector...withDelay:|. This is not desirable in
+// unit tests because it's hard to test around run loops in a deterministic
+// manner. To avoid those headaches, tests should provide an alternative
+// implementation.
+- (void)dispatchCommandInternal:(NSInteger)tag;
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_WRENCH_MENU_CONTROLLER_H_
diff --git a/chrome/browser/ui/cocoa/wrench_menu_controller.mm b/chrome/browser/ui/cocoa/wrench_menu_controller.mm
new file mode 100644
index 0000000..d4a9872
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu_controller.mm
@@ -0,0 +1,213 @@
+// 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/ui/cocoa/wrench_menu_controller.h"
+
+#include "app/l10n_util.h"
+#include "app/menus/menu_model.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h"
+#import "chrome/browser/ui/cocoa/menu_tracked_root_view.h"
+#import "chrome/browser/ui/cocoa/toolbar_controller.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/wrench_menu_model.h"
+#include "chrome/common/notification_observer.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/notification_source.h"
+#include "chrome/common/notification_type.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+
+@interface WrenchMenuController (Private)
+- (void)adjustPositioning;
+- (void)performCommandDispatch:(NSNumber*)tag;
+- (NSButton*)zoomDisplay;
+@end
+
+namespace WrenchMenuControllerInternal {
+
+class ZoomLevelObserver : public NotificationObserver {
+ public:
+ explicit ZoomLevelObserver(WrenchMenuController* controller)
+ : controller_(controller) {
+ registrar_.Add(this, NotificationType::ZOOM_LEVEL_CHANGED,
+ NotificationService::AllSources());
+ }
+
+ void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ DCHECK_EQ(type.value, NotificationType::ZOOM_LEVEL_CHANGED);
+ WrenchMenuModel* wrenchMenuModel = [controller_ wrenchMenuModel];
+ wrenchMenuModel->UpdateZoomControls();
+ const string16 level =
+ wrenchMenuModel->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY);
+ [[controller_ zoomDisplay] setTitle:SysUTF16ToNSString(level)];
+ }
+
+ private:
+ NotificationRegistrar registrar_;
+ WrenchMenuController* controller_; // Weak; owns this.
+};
+
+} // namespace WrenchMenuControllerInternal
+
+@implementation WrenchMenuController
+
+- (id)init {
+ if ((self = [super init])) {
+ observer_.reset(new WrenchMenuControllerInternal::ZoomLevelObserver(self));
+ }
+ return self;
+}
+
+- (void)addItemToMenu:(NSMenu*)menu
+ atIndex:(NSInteger)index
+ fromModel:(menus::MenuModel*)model
+ modelIndex:(int)modelIndex {
+ // Non-button item types should be built as normal items.
+ menus::MenuModel::ItemType type = model->GetTypeAt(modelIndex);
+ if (type != menus::MenuModel::TYPE_BUTTON_ITEM) {
+ [super addItemToMenu:menu
+ atIndex:index
+ fromModel:model
+ modelIndex:modelIndex];
+ return;
+ }
+
+ // Handle the special-cased menu items.
+ int command_id = model->GetCommandIdAt(modelIndex);
+ scoped_nsobject<NSMenuItem> customItem(
+ [[NSMenuItem alloc] initWithTitle:@""
+ action:nil
+ keyEquivalent:@""]);
+ switch (command_id) {
+ case IDC_EDIT_MENU:
+ DCHECK(editItem_);
+ [customItem setView:editItem_];
+ [editItem_ setMenuItem:customItem];
+ break;
+ case IDC_ZOOM_MENU:
+ DCHECK(zoomItem_);
+ [customItem setView:zoomItem_];
+ [zoomItem_ setMenuItem:customItem];
+ break;
+ default:
+ NOTREACHED();
+ break;
+ }
+ [self adjustPositioning];
+ [menu insertItem:customItem.get() atIndex:index];
+}
+
+- (NSMenu*)menu {
+ NSMenu* menu = [super menu];
+ if (![menu delegate]) {
+ [menu setDelegate:self];
+ }
+ return menu;
+}
+
+- (void)menuWillOpen:(NSMenu*)menu {
+ NSString* title = base::SysUTF16ToNSString(
+ [self wrenchMenuModel]->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY));
+ [[zoomItem_ viewWithTag:IDC_ZOOM_PERCENT_DISPLAY] setTitle:title];
+
+ NSImage* icon = [self wrenchMenuModel]->browser()->window()->IsFullscreen() ?
+ [NSImage imageNamed:NSImageNameExitFullScreenTemplate] :
+ [NSImage imageNamed:NSImageNameEnterFullScreenTemplate];
+ [zoomFullScreen_ setImage:icon];
+}
+
+// Used to dispatch commands from the Wrench menu. The custom items within the
+// menu cannot be hooked up directly to First Responder because the window in
+// which the controls reside is not the BrowserWindowController, but a
+// NSCarbonMenuWindow; this screws up the typical |-commandDispatch:| system.
+- (IBAction)dispatchWrenchMenuCommand:(id)sender {
+ NSInteger tag = [sender tag];
+ if (sender == zoomPlus_ || sender == zoomMinus_) {
+ // Do a direct dispatch rather than scheduling on the outermost run loop,
+ // which would not get hit until after the menu had closed.
+ [self performCommandDispatch:[NSNumber numberWithInt:tag]];
+
+ // The zoom buttons should not close the menu if opened sticky.
+ if ([sender respondsToSelector:@selector(isTracking)] &&
+ [sender performSelector:@selector(isTracking)]) {
+ [menu_ cancelTracking];
+ }
+ } else {
+ // The custom views within the Wrench menu are abnormal and keep the menu
+ // open after a target-action. Close the menu manually.
+ [menu_ cancelTracking];
+ [self dispatchCommandInternal:tag];
+ }
+}
+
+- (void)dispatchCommandInternal:(NSInteger)tag {
+ // Executing certain commands from the nested run loop of the menu can lead
+ // to wonky behavior (e.g. http://crbug.com/49716). To avoid this, schedule
+ // the dispatch on the outermost run loop.
+ [self performSelector:@selector(performCommandDispatch:)
+ withObject:[NSNumber numberWithInt:tag]
+ afterDelay:0.0];
+}
+
+// Used to perform the actual dispatch on the outermost runloop.
+- (void)performCommandDispatch:(NSNumber*)tag {
+ [self wrenchMenuModel]->ExecuteCommand([tag intValue]);
+}
+
+- (WrenchMenuModel*)wrenchMenuModel {
+ return static_cast<WrenchMenuModel*>(model_);
+}
+
+// Fit the localized strings into the Cut/Copy/Paste control, then resize the
+// whole menu item accordingly.
+- (void)adjustPositioning {
+ const CGFloat kButtonPadding = 12;
+ CGFloat delta = 0;
+
+ // Go through the three buttons from right-to-left, adjusting the size to fit
+ // the localized strings while keeping them all aligned on their horizontal
+ // edges.
+ const size_t kAdjustViewCount = 3;
+ NSButton* views[kAdjustViewCount] = { editPaste_, editCopy_, editCut_ };
+ for (size_t i = 0; i < kAdjustViewCount; ++i) {
+ NSButton* button = views[i];
+ CGFloat originalWidth = NSWidth([button frame]);
+
+ // Do not let |-sizeToFit| change the height of the button.
+ NSSize size = [button frame].size;
+ [button sizeToFit];
+ size.width = [button frame].size.width + kButtonPadding;
+ [button setFrameSize:size];
+
+ CGFloat newWidth = size.width;
+ delta += newWidth - originalWidth;
+
+ NSRect frame = [button frame];
+ frame.origin.x -= delta;
+ [button setFrame:frame];
+ }
+
+ // Resize the menu item by the total amound the buttons changed so that the
+ // spacing between the buttons and the title remains the same.
+ NSRect itemFrame = [editItem_ frame];
+ itemFrame.size.width += delta;
+ [editItem_ setFrame:itemFrame];
+
+ // Also resize the superview of the buttons, which is an NSView used to slide
+ // when the item title is too big and GTM resizes it.
+ NSRect parentFrame = [[editCut_ superview] frame];
+ parentFrame.size.width += delta;
+ parentFrame.origin.x -= delta;
+ [[editCut_ superview] setFrame:parentFrame];
+}
+
+- (NSButton*)zoomDisplay {
+ return zoomDisplay_;
+}
+
+@end // @implementation WrenchMenuController
diff --git a/chrome/browser/ui/cocoa/wrench_menu_controller_unittest.mm b/chrome/browser/ui/cocoa/wrench_menu_controller_unittest.mm
new file mode 100644
index 0000000..243b2af
--- /dev/null
+++ b/chrome/browser/ui/cocoa/wrench_menu_controller_unittest.mm
@@ -0,0 +1,84 @@
+// 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"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/wrench_menu_model.h"
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/toolbar_controller.h"
+#import "chrome/browser/ui/cocoa/wrench_menu_controller.h"
+#import "chrome/browser/ui/cocoa/view_resizer_pong.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+// Override to avoid dealing with run loops in the testing environment.
+@implementation WrenchMenuController (UnitTesting)
+- (void)dispatchCommandInternal:(NSInteger)tag {
+ [self wrenchMenuModel]->ExecuteCommand(tag);
+}
+@end
+
+
+namespace {
+
+class MockWrenchMenuModel : public WrenchMenuModel {
+ public:
+ MockWrenchMenuModel() : WrenchMenuModel() {}
+ ~MockWrenchMenuModel() {
+ // This dirty, ugly hack gets around a bug in the test. In
+ // ~WrenchMenuModel(), there's a call to TabstripModel::RemoveObserver(this)
+ // which mysteriously leads to this crash: http://crbug.com/49206 . It
+ // seems that the vector of observers is getting hosed somewhere between
+ // |-[ToolbarController dealloc]| and ~MockWrenchMenuModel(). This line
+ // short-circuits the parent destructor to avoid this crash.
+ tabstrip_model_ = NULL;
+ }
+ MOCK_METHOD1(ExecuteCommand, void(int command_id));
+};
+
+class WrenchMenuControllerTest : public CocoaTest {
+ public:
+ void SetUp() {
+ Browser* browser = helper_.browser();
+ resize_delegate_.reset([[ViewResizerPong alloc] init]);
+ toolbar_controller_.reset(
+ [[ToolbarController alloc] initWithModel:browser->toolbar_model()
+ commands:browser->command_updater()
+ profile:helper_.profile()
+ browser:browser
+ resizeDelegate:resize_delegate_.get()]);
+ EXPECT_TRUE([toolbar_controller_ view]);
+ NSView* parent = [test_window() contentView];
+ [parent addSubview:[toolbar_controller_ view]];
+ }
+
+ WrenchMenuController* controller() {
+ return [toolbar_controller_ wrenchMenuController];
+ }
+
+ BrowserTestHelper helper_;
+ scoped_nsobject<ViewResizerPong> resize_delegate_;
+ MockWrenchMenuModel fake_model_;
+ scoped_nsobject<ToolbarController> toolbar_controller_;
+};
+
+TEST_F(WrenchMenuControllerTest, Initialized) {
+ EXPECT_TRUE([controller() menu]);
+ EXPECT_GE([[controller() menu] numberOfItems], 5);
+}
+
+TEST_F(WrenchMenuControllerTest, DispatchSimple) {
+ scoped_nsobject<NSButton> button([[NSButton alloc] init]);
+ [button setTag:IDC_ZOOM_PLUS];
+
+ // Set fake model to test dispatching.
+ EXPECT_CALL(fake_model_, ExecuteCommand(IDC_ZOOM_PLUS));
+ [controller() setModel:&fake_model_];
+
+ [controller() dispatchWrenchMenuCommand:button.get()];
+}
+
+} // namespace