1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "chrome/browser/ui/gtk/reload_button_gtk.h" 6 7#include <algorithm> 8 9#include "base/debug/trace_event.h" 10#include "base/logging.h" 11#include "base/message_loop/message_loop.h" 12#include "chrome/app/chrome_command_ids.h" 13#include "chrome/browser/chrome_notification_types.h" 14#include "chrome/browser/ui/browser.h" 15#include "chrome/browser/ui/browser_commands.h" 16#include "chrome/browser/ui/gtk/accelerators_gtk.h" 17#include "chrome/browser/ui/gtk/event_utils.h" 18#include "chrome/browser/ui/gtk/gtk_chrome_button.h" 19#include "chrome/browser/ui/gtk/gtk_theme_service.h" 20#include "chrome/browser/ui/gtk/gtk_util.h" 21#include "chrome/browser/ui/gtk/location_bar_view_gtk.h" 22#include "content/public/browser/notification_source.h" 23#include "grit/generated_resources.h" 24#include "grit/theme_resources.h" 25#include "ui/base/l10n/l10n_util.h" 26 27// The width of this button in GTK+ theme mode. The Stop and Refresh stock icons 28// can be different sizes; this variable is used to make sure that the button 29// doesn't change sizes when switching between the two. 30static int GtkButtonWidth = 0; 31 32// The time in milliseconds between when the user clicks and the menu appears. 33static const int kReloadMenuTimerDelay = 500; 34 35// Content of the Reload drop-down menu. 36static const int kReloadMenuItems[] = { 37 IDS_RELOAD_MENU_NORMAL_RELOAD_ITEM, 38 IDS_RELOAD_MENU_HARD_RELOAD_ITEM, 39 IDS_RELOAD_MENU_EMPTY_AND_HARD_RELOAD_ITEM, 40}; 41 42//////////////////////////////////////////////////////////////////////////////// 43// ReloadButton, public: 44 45ReloadButtonGtk::ReloadButtonGtk(LocationBarViewGtk* location_bar, 46 Browser* browser) 47 : location_bar_(location_bar), 48 browser_(browser), 49 intended_mode_(MODE_RELOAD), 50 visible_mode_(MODE_RELOAD), 51 theme_service_(browser ? 52 GtkThemeService::GetFrom(browser->profile()) : NULL), 53 reload_(theme_service_, IDR_RELOAD, IDR_RELOAD_P, IDR_RELOAD_H, 0), 54 stop_(theme_service_, IDR_STOP, IDR_STOP_P, IDR_STOP_H, IDR_STOP_D), 55 widget_(gtk_chrome_button_new()), 56 stop_to_reload_timer_delay_(base::TimeDelta::FromMilliseconds(1350)), 57 menu_visible_(false), 58 testing_mouse_hovered_(false), 59 testing_reload_count_(0), 60 weak_factory_(this) { 61 menu_model_.reset(new ui::SimpleMenuModel(this)); 62 for (size_t i = 0; i < arraysize(kReloadMenuItems); i++) { 63 menu_model_->AddItemWithStringId(kReloadMenuItems[i], kReloadMenuItems[i]); 64 } 65 66 gtk_widget_set_size_request(widget(), reload_.Width(), reload_.Height()); 67 68 gtk_widget_set_app_paintable(widget(), TRUE); 69 70 g_signal_connect(widget(), "clicked", G_CALLBACK(OnClickedThunk), this); 71 g_signal_connect(widget(), "expose-event", G_CALLBACK(OnExposeThunk), this); 72 g_signal_connect(widget(), "leave-notify-event", 73 G_CALLBACK(OnLeaveNotifyThunk), this); 74 gtk_widget_set_can_focus(widget(), FALSE); 75 76 gtk_widget_set_has_tooltip(widget(), TRUE); 77 g_signal_connect(widget(), "query-tooltip", G_CALLBACK(OnQueryTooltipThunk), 78 this); 79 80 g_signal_connect(widget(), "button-press-event", 81 G_CALLBACK(OnButtonPressThunk), this); 82 gtk_widget_add_events(widget(), GDK_POINTER_MOTION_MASK); 83 g_signal_connect(widget(), "motion-notify-event", 84 G_CALLBACK(OnMouseMoveThunk), this); 85 86 // Popup the menu as left-aligned relative to this widget rather than the 87 // default of right aligned. 88 g_object_set_data(G_OBJECT(widget()), "left-align-popup", 89 reinterpret_cast<void*>(true)); 90 91 hover_controller_.Init(widget()); 92 gtk_util::SetButtonTriggersNavigation(widget()); 93 94 if (theme_service_) { 95 theme_service_->InitThemesFor(this); 96 registrar_.Add(this, 97 chrome::NOTIFICATION_BROWSER_THEME_CHANGED, 98 content::Source<ThemeService>(theme_service_)); 99 } 100 101 // Set the default double-click timer delay to the system double-click time. 102 int timer_delay_ms; 103 GtkSettings* settings = gtk_settings_get_default(); 104 g_object_get(G_OBJECT(settings), "gtk-double-click-time", &timer_delay_ms, 105 NULL); 106 double_click_timer_delay_ = base::TimeDelta::FromMilliseconds(timer_delay_ms); 107} 108 109ReloadButtonGtk::~ReloadButtonGtk() { 110 widget_.Destroy(); 111} 112 113void ReloadButtonGtk::ChangeMode(Mode mode, bool force) { 114 intended_mode_ = mode; 115 116 // If the change is forced, or the user isn't hovering the icon, or it's safe 117 // to change it to the other image type, make the change immediately; 118 // otherwise we'll let it happen later. 119 if (force || ((gtk_widget_get_state(widget()) == GTK_STATE_NORMAL) && 120 !testing_mouse_hovered_) || ((mode == MODE_STOP) ? 121 !double_click_timer_.IsRunning() : (visible_mode_ != MODE_STOP))) { 122 double_click_timer_.Stop(); 123 stop_to_reload_timer_.Stop(); 124 visible_mode_ = mode; 125 126 // Do not change the state of the button if menu is currently visible. 127 if (!menu_visible_) { 128 stop_.set_paint_override(-1); 129 gtk_chrome_button_unset_paint_state(GTK_CHROME_BUTTON(widget_.get())); 130 } 131 132 UpdateThemeButtons(); 133 gtk_widget_queue_draw(widget()); 134 } else if (visible_mode_ != MODE_RELOAD) { 135 // If you read the views implementation of reload_button.cc, you'll see 136 // that instead of screwing with paint states, the views implementation 137 // just changes whether the view is enabled. We can't do that here because 138 // changing the widget state to GTK_STATE_INSENSITIVE will cause a cascade 139 // of messages on all its children and will also trigger a synthesized 140 // leave notification and prevent the real leave notification from turning 141 // the button back to normal. So instead, override the stop_ paint state 142 // for chrome-theme mode, and use this as a flag to discard click events. 143 stop_.set_paint_override(GTK_STATE_INSENSITIVE); 144 145 // Also set the gtk_chrome_button paint state to insensitive to hide 146 // the border drawn around the stop icon. 147 gtk_chrome_button_set_paint_state(GTK_CHROME_BUTTON(widget_.get()), 148 GTK_STATE_INSENSITIVE); 149 150 // If we're in GTK theme mode, we need to also render the correct icon for 151 // the stop/insensitive since we won't be using |stop_| to render the icon. 152 UpdateThemeButtons(); 153 154 // Go ahead and change to reload after a bit, which allows repeated reloads 155 // without moving the mouse. 156 if (!stop_to_reload_timer_.IsRunning()) { 157 stop_to_reload_timer_.Start(FROM_HERE, stop_to_reload_timer_delay_, this, 158 &ReloadButtonGtk::OnStopToReloadTimer); 159 } 160 } 161} 162 163//////////////////////////////////////////////////////////////////////////////// 164// ReloadButtonGtk, content::NotificationObserver implementation: 165 166void ReloadButtonGtk::Observe(int type, 167 const content::NotificationSource& source, 168 const content::NotificationDetails& details) { 169 DCHECK(chrome::NOTIFICATION_BROWSER_THEME_CHANGED == type); 170 171 GtkThemeService* provider = static_cast<GtkThemeService*>( 172 content::Source<ThemeService>(source).ptr()); 173 DCHECK_EQ(provider, theme_service_); 174 GtkButtonWidth = 0; 175 UpdateThemeButtons(); 176} 177 178//////////////////////////////////////////////////////////////////////////////// 179// ReloadButtonGtk, MenuGtk::Delegate implementation: 180 181void ReloadButtonGtk::StoppedShowing() { 182 menu_visible_ = false; 183 ChangeMode(intended_mode_, true); 184} 185 186//////////////////////////////////////////////////////////////////////////////// 187// ReloadButtonGtk, SimpleMenuModel::Delegate implementation: 188 189bool ReloadButtonGtk::IsCommandIdChecked(int command_id) const { 190 return false; 191} 192 193bool ReloadButtonGtk::IsCommandIdEnabled(int command_id) const { 194 return true; 195} 196 197bool ReloadButtonGtk::IsCommandIdVisible(int command_id) const { 198 return true; 199} 200 201bool ReloadButtonGtk::GetAcceleratorForCommandId( 202 int command_id, 203 ui::Accelerator* out_accelerator) { 204 int command = 0; 205 switch (command_id) { 206 case IDS_RELOAD_MENU_NORMAL_RELOAD_ITEM: 207 command = IDC_RELOAD; 208 break; 209 case IDS_RELOAD_MENU_HARD_RELOAD_ITEM: 210 command = IDC_RELOAD_IGNORING_CACHE; 211 break; 212 case IDS_RELOAD_MENU_EMPTY_AND_HARD_RELOAD_ITEM: 213 // No accelerator. 214 break; 215 default: 216 LOG(ERROR) << "Unknown reload menu command"; 217 } 218 219 if (command) { 220 const ui::Accelerator* accelerator = 221 AcceleratorsGtk::GetInstance()-> 222 GetPrimaryAcceleratorForCommand(command); 223 if (accelerator) { 224 *out_accelerator = *accelerator; 225 return true; 226 } 227 } 228 return false; 229} 230 231void ReloadButtonGtk::ExecuteCommand(int command_id, int event_flags) { 232 switch (command_id) { 233 case IDS_RELOAD_MENU_NORMAL_RELOAD_ITEM: 234 DoReload(IDC_RELOAD); 235 break; 236 case IDS_RELOAD_MENU_HARD_RELOAD_ITEM: 237 DoReload(IDC_RELOAD_IGNORING_CACHE); 238 break; 239 case IDS_RELOAD_MENU_EMPTY_AND_HARD_RELOAD_ITEM: 240 ClearCache(); 241 DoReload(IDC_RELOAD_IGNORING_CACHE); 242 break; 243 default: 244 LOG(ERROR) << "Unknown reload menu command"; 245 } 246} 247 248//////////////////////////////////////////////////////////////////////////////// 249// ReloadButtonGtk, private: 250 251void ReloadButtonGtk::OnClicked(GtkWidget* /* sender */) { 252 weak_factory_.InvalidateWeakPtrs(); 253 if (visible_mode_ == MODE_STOP) { 254 // Do nothing if Stop was disabled due to an attempt to change back to 255 // RELOAD mode while hovered. 256 if (stop_.paint_override() == GTK_STATE_INSENSITIVE) 257 return; 258 259 if (browser_) 260 chrome::Stop(browser_); 261 262 // The user has clicked, so we can feel free to update the button, 263 // even if the mouse is still hovering. 264 ChangeMode(MODE_RELOAD, true); 265 } else if (!double_click_timer_.IsRunning()) { 266 DoReload(0); 267 } 268} 269 270gboolean ReloadButtonGtk::OnExpose(GtkWidget* widget, 271 GdkEventExpose* e) { 272 TRACE_EVENT0("ui::gtk", "ReloadButtonGtk::OnExpose"); 273 if (theme_service_ && theme_service_->UsingNativeTheme()) 274 return FALSE; 275 return ((visible_mode_ == MODE_RELOAD) ? reload_ : stop_).OnExpose( 276 widget, e, hover_controller_.GetCurrentValue()); 277} 278 279gboolean ReloadButtonGtk::OnLeaveNotify(GtkWidget* /* widget */, 280 GdkEventCrossing* /* event */) { 281 ChangeMode(intended_mode_, true); 282 return FALSE; 283} 284 285gboolean ReloadButtonGtk::OnQueryTooltip(GtkWidget* /* sender */, 286 gint /* x */, 287 gint /* y */, 288 gboolean /* keyboard_mode */, 289 GtkTooltip* tooltip) { 290 // |location_bar_| can be NULL in tests. 291 if (!location_bar_) 292 return FALSE; 293 294 int reload_tooltip = ReloadMenuEnabled() ? 295 IDS_TOOLTIP_RELOAD_WITH_MENU : IDS_TOOLTIP_RELOAD; 296 gtk_tooltip_set_text(tooltip, l10n_util::GetStringUTF8( 297 (visible_mode_ == MODE_RELOAD) ? 298 reload_tooltip : IDS_TOOLTIP_STOP).c_str()); 299 return TRUE; 300} 301 302gboolean ReloadButtonGtk::OnButtonPress(GtkWidget* widget, 303 GdkEventButton* event) { 304 if (!ReloadMenuEnabled() || visible_mode_ == MODE_STOP) 305 return FALSE; 306 307 if (event->button == 3) 308 ShowReloadMenu(event->button, event->time); 309 310 if (event->button != 1) 311 return FALSE; 312 313 y_position_of_last_press_ = static_cast<int>(event->y); 314 base::MessageLoop::current()->PostDelayedTask( 315 FROM_HERE, 316 base::Bind(&ReloadButtonGtk::ShowReloadMenu, 317 weak_factory_.GetWeakPtr(), 318 event->button, 319 event->time), 320 base::TimeDelta::FromMilliseconds(kReloadMenuTimerDelay)); 321 return FALSE; 322} 323 324gboolean ReloadButtonGtk::OnMouseMove(GtkWidget* widget, 325 GdkEventMotion* event) { 326 // If we aren't waiting to show the back forward menu, do nothing. 327 if (!weak_factory_.HasWeakPtrs()) 328 return FALSE; 329 330 // We only count moves about a certain threshold. 331 GtkSettings* settings = gtk_widget_get_settings(widget); 332 int drag_min_distance; 333 g_object_get(settings, "gtk-dnd-drag-threshold", &drag_min_distance, NULL); 334 if (event->y - y_position_of_last_press_ < drag_min_distance) 335 return FALSE; 336 337 // We will show the menu now. Cancel the delayed event. 338 weak_factory_.InvalidateWeakPtrs(); 339 ShowReloadMenu(/* button */ 1, event->time); 340 return FALSE; 341} 342 343void ReloadButtonGtk::UpdateThemeButtons() { 344 bool use_gtk = theme_service_ && theme_service_->UsingNativeTheme(); 345 346 if (use_gtk) { 347 gtk_widget_ensure_style(widget()); 348 GtkStyle* style = gtk_widget_get_style(widget()); 349 GtkIconSet* icon_set = gtk_style_lookup_icon_set( 350 style, 351 (visible_mode_ == MODE_RELOAD) ? GTK_STOCK_REFRESH : GTK_STOCK_STOP); 352 if (icon_set) { 353 GtkStateType state = gtk_widget_get_state(widget()); 354 if (visible_mode_ == MODE_STOP && stop_.paint_override() != -1) 355 state = static_cast<GtkStateType>(stop_.paint_override()); 356 357 GdkPixbuf* pixbuf = gtk_icon_set_render_icon( 358 icon_set, 359 style, 360 gtk_widget_get_direction(widget()), 361 state, 362 GTK_ICON_SIZE_SMALL_TOOLBAR, 363 widget(), 364 NULL); 365 366 gtk_button_set_image(GTK_BUTTON(widget()), 367 gtk_image_new_from_pixbuf(pixbuf)); 368 g_object_unref(pixbuf); 369 } 370 371 gtk_widget_set_size_request(widget(), -1, -1); 372 GtkRequisition req; 373 gtk_widget_size_request(widget(), &req); 374 GtkButtonWidth = std::max(GtkButtonWidth, req.width); 375 gtk_widget_set_size_request(widget(), GtkButtonWidth, -1); 376 377 gtk_widget_set_app_paintable(widget(), FALSE); 378 gtk_widget_set_double_buffered(widget(), TRUE); 379 } else { 380 gtk_button_set_image(GTK_BUTTON(widget()), NULL); 381 382 gtk_widget_set_size_request(widget(), reload_.Width(), reload_.Height()); 383 384 gtk_widget_set_app_paintable(widget(), TRUE); 385 // We effectively double-buffer by virtue of having only one image... 386 gtk_widget_set_double_buffered(widget(), FALSE); 387 } 388 389 gtk_chrome_button_set_use_gtk_rendering(GTK_CHROME_BUTTON(widget()), use_gtk); 390} 391 392void ReloadButtonGtk::OnDoubleClickTimer() { 393 ChangeMode(intended_mode_, false); 394} 395 396void ReloadButtonGtk::OnStopToReloadTimer() { 397 ChangeMode(intended_mode_, true); 398} 399 400void ReloadButtonGtk::ShowReloadMenu(int button, guint32 event_time) { 401 if (!ReloadMenuEnabled() || visible_mode_ == MODE_STOP) 402 return; 403 404 menu_visible_ = true; 405 menu_.reset(new MenuGtk(this, menu_model_.get())); 406 reload_.set_paint_override(GTK_STATE_ACTIVE); 407 gtk_chrome_button_set_paint_state(GTK_CHROME_BUTTON(widget_.get()), 408 GTK_STATE_ACTIVE); 409 gtk_widget_queue_draw(widget()); 410 menu_->PopupForWidget(widget(), button, event_time); 411} 412 413void ReloadButtonGtk::DoReload(int command) { 414 // Shift-clicking or Ctrl-clicking the reload button means we should ignore 415 // any cached content. 416 GdkModifierType modifier_state; 417 gtk_get_current_event_state(&modifier_state); 418 guint modifier_state_uint = modifier_state; 419 420 // Default reload behaviour. 421 if (command == 0) { 422 if (modifier_state_uint & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) { 423 command = IDC_RELOAD_IGNORING_CACHE; 424 // Mask off Shift and Control so they don't affect the disposition below. 425 modifier_state_uint &= ~(GDK_SHIFT_MASK | GDK_CONTROL_MASK); 426 } else { 427 command = IDC_RELOAD; 428 } 429 } 430 431 WindowOpenDisposition disposition = 432 event_utils::DispositionFromGdkState(modifier_state_uint); 433 if ((disposition == CURRENT_TAB) && location_bar_) { 434 // Forcibly reset the location bar, since otherwise it won't discard any 435 // ongoing user edits, since it doesn't realize this is a user-initiated 436 // action. 437 location_bar_->Revert(); 438 } 439 440 // Start a timer - while this timer is running, the reload button cannot be 441 // changed to a stop button. We do not set |intended_mode_| to MODE_STOP 442 // here as the browser will do that when it actually starts loading (which 443 // may happen synchronously, thus the need to do this before telling the 444 // browser to execute the reload command). 445 double_click_timer_.Start(FROM_HERE, double_click_timer_delay_, this, 446 &ReloadButtonGtk::OnDoubleClickTimer); 447 448 if (browser_) 449 chrome::ExecuteCommandWithDisposition(browser_, command, disposition); 450 ++testing_reload_count_; 451} 452 453bool ReloadButtonGtk::ReloadMenuEnabled() { 454 if (!browser_) 455 return false; 456 return chrome::IsDebuggerAttachedToCurrentTab(browser_); 457} 458 459void ReloadButtonGtk::ClearCache() { 460 if (browser_) 461 chrome::ClearCache(browser_); 462} 463