1 // Copyright (c) 2011 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/logging.h" 10 #include "chrome/app/chrome_command_ids.h" 11 #include "chrome/browser/ui/browser.h" 12 #include "chrome/browser/ui/gtk/gtk_chrome_button.h" 13 #include "chrome/browser/ui/gtk/gtk_theme_service.h" 14 #include "chrome/browser/ui/gtk/gtk_util.h" 15 #include "chrome/browser/ui/gtk/location_bar_view_gtk.h" 16 #include "content/common/notification_source.h" 17 #include "grit/generated_resources.h" 18 #include "grit/theme_resources.h" 19 #include "ui/base/l10n/l10n_util.h" 20 21 // The width of this button in GTK+ theme mode. The Stop and Refresh stock icons 22 // can be different sizes; this variable is used to make sure that the button 23 // doesn't change sizes when switching between the two. 24 static int GtkButtonWidth = 0; 25 26 //////////////////////////////////////////////////////////////////////////////// 27 // ReloadButton, public: 28 29 ReloadButtonGtk::ReloadButtonGtk(LocationBarViewGtk* location_bar, 30 Browser* browser) 31 : location_bar_(location_bar), 32 browser_(browser), 33 intended_mode_(MODE_RELOAD), 34 visible_mode_(MODE_RELOAD), 35 theme_service_(browser ? 36 GtkThemeService::GetFrom(browser->profile()) : NULL), 37 reload_(theme_service_, IDR_RELOAD, IDR_RELOAD_P, IDR_RELOAD_H, 0), 38 stop_(theme_service_, IDR_STOP, IDR_STOP_P, IDR_STOP_H, IDR_STOP_D), 39 widget_(gtk_chrome_button_new()), 40 stop_to_reload_timer_delay_(base::TimeDelta::FromMilliseconds(1350)), 41 testing_mouse_hovered_(false), 42 testing_reload_count_(0) { 43 gtk_widget_set_size_request(widget(), reload_.Width(), reload_.Height()); 44 45 gtk_widget_set_app_paintable(widget(), TRUE); 46 47 g_signal_connect(widget(), "clicked", G_CALLBACK(OnClickedThunk), this); 48 g_signal_connect(widget(), "expose-event", G_CALLBACK(OnExposeThunk), this); 49 g_signal_connect(widget(), "leave-notify-event", 50 G_CALLBACK(OnLeaveNotifyThunk), this); 51 GTK_WIDGET_UNSET_FLAGS(widget(), GTK_CAN_FOCUS); 52 53 gtk_widget_set_has_tooltip(widget(), TRUE); 54 g_signal_connect(widget(), "query-tooltip", G_CALLBACK(OnQueryTooltipThunk), 55 this); 56 57 hover_controller_.Init(widget()); 58 gtk_util::SetButtonTriggersNavigation(widget()); 59 60 if (theme_service_) { 61 theme_service_->InitThemesFor(this); 62 registrar_.Add(this, 63 NotificationType::BROWSER_THEME_CHANGED, 64 Source<ThemeService>(theme_service_)); 65 } 66 67 // Set the default double-click timer delay to the system double-click time. 68 int timer_delay_ms; 69 GtkSettings* settings = gtk_settings_get_default(); 70 g_object_get(G_OBJECT(settings), "gtk-double-click-time", &timer_delay_ms, 71 NULL); 72 double_click_timer_delay_ = base::TimeDelta::FromMilliseconds(timer_delay_ms); 73 } 74 75 ReloadButtonGtk::~ReloadButtonGtk() { 76 widget_.Destroy(); 77 } 78 79 void ReloadButtonGtk::ChangeMode(Mode mode, bool force) { 80 intended_mode_ = mode; 81 82 // If the change is forced, or the user isn't hovering the icon, or it's safe 83 // to change it to the other image type, make the change immediately; 84 // otherwise we'll let it happen later. 85 if (force || ((GTK_WIDGET_STATE(widget()) == GTK_STATE_NORMAL) && 86 !testing_mouse_hovered_) || ((mode == MODE_STOP) ? 87 !double_click_timer_.IsRunning() : (visible_mode_ != MODE_STOP))) { 88 double_click_timer_.Stop(); 89 stop_to_reload_timer_.Stop(); 90 visible_mode_ = mode; 91 92 stop_.set_paint_override(-1); 93 gtk_chrome_button_unset_paint_state(GTK_CHROME_BUTTON(widget_.get())); 94 95 UpdateThemeButtons(); 96 gtk_widget_queue_draw(widget()); 97 } else if (visible_mode_ != MODE_RELOAD) { 98 // If you read the views implementation of reload_button.cc, you'll see 99 // that instead of screwing with paint states, the views implementation 100 // just changes whether the view is enabled. We can't do that here because 101 // changing the widget state to GTK_STATE_INSENSITIVE will cause a cascade 102 // of messages on all its children and will also trigger a synthesized 103 // leave notification and prevent the real leave notification from turning 104 // the button back to normal. So instead, override the stop_ paint state 105 // for chrome-theme mode, and use this as a flag to discard click events. 106 stop_.set_paint_override(GTK_STATE_INSENSITIVE); 107 108 // Also set the gtk_chrome_button paint state to insensitive to hide 109 // the border drawn around the stop icon. 110 gtk_chrome_button_set_paint_state(GTK_CHROME_BUTTON(widget_.get()), 111 GTK_STATE_INSENSITIVE); 112 113 // If we're in GTK theme mode, we need to also render the correct icon for 114 // the stop/insensitive since we won't be using |stop_| to render the icon. 115 UpdateThemeButtons(); 116 117 // Go ahead and change to reload after a bit, which allows repeated reloads 118 // without moving the mouse. 119 if (!stop_to_reload_timer_.IsRunning()) { 120 stop_to_reload_timer_.Start(stop_to_reload_timer_delay_, this, 121 &ReloadButtonGtk::OnStopToReloadTimer); 122 } 123 } 124 } 125 126 //////////////////////////////////////////////////////////////////////////////// 127 // ReloadButtonGtk, NotificationObserver implementation: 128 129 void ReloadButtonGtk::Observe(NotificationType type, 130 const NotificationSource& source, 131 const NotificationDetails& /* details */) { 132 DCHECK(NotificationType::BROWSER_THEME_CHANGED == type); 133 134 GtkThemeService* provider = static_cast<GtkThemeService*>( 135 Source<ThemeService>(source).ptr()); 136 DCHECK_EQ(provider, theme_service_); 137 GtkButtonWidth = 0; 138 UpdateThemeButtons(); 139 } 140 141 //////////////////////////////////////////////////////////////////////////////// 142 // ReloadButtonGtk, private: 143 144 void ReloadButtonGtk::OnClicked(GtkWidget* /* sender */) { 145 if (visible_mode_ == MODE_STOP) { 146 // Do nothing if Stop was disabled due to an attempt to change back to 147 // RELOAD mode while hovered. 148 if (stop_.paint_override() == GTK_STATE_INSENSITIVE) 149 return; 150 151 if (browser_) 152 browser_->Stop(); 153 154 // The user has clicked, so we can feel free to update the button, 155 // even if the mouse is still hovering. 156 ChangeMode(MODE_RELOAD, true); 157 } else if (!double_click_timer_.IsRunning()) { 158 // Shift-clicking or Ctrl-clicking the reload button means we should ignore 159 // any cached content. 160 int command; 161 GdkModifierType modifier_state; 162 gtk_get_current_event_state(&modifier_state); 163 guint modifier_state_uint = modifier_state; 164 if (modifier_state_uint & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) { 165 command = IDC_RELOAD_IGNORING_CACHE; 166 // Mask off Shift and Control so they don't affect the disposition below. 167 modifier_state_uint &= ~(GDK_SHIFT_MASK | GDK_CONTROL_MASK); 168 } else { 169 command = IDC_RELOAD; 170 } 171 172 WindowOpenDisposition disposition = 173 event_utils::DispositionFromEventFlags(modifier_state_uint); 174 if ((disposition == CURRENT_TAB) && location_bar_) { 175 // Forcibly reset the location bar, since otherwise it won't discard any 176 // ongoing user edits, since it doesn't realize this is a user-initiated 177 // action. 178 location_bar_->Revert(); 179 } 180 181 // Start a timer - while this timer is running, the reload button cannot be 182 // changed to a stop button. We do not set |intended_mode_| to MODE_STOP 183 // here as the browser will do that when it actually starts loading (which 184 // may happen synchronously, thus the need to do this before telling the 185 // browser to execute the reload command). 186 double_click_timer_.Start(double_click_timer_delay_, this, 187 &ReloadButtonGtk::OnDoubleClickTimer); 188 189 if (browser_) 190 browser_->ExecuteCommandWithDisposition(command, disposition); 191 ++testing_reload_count_; 192 } 193 } 194 195 gboolean ReloadButtonGtk::OnExpose(GtkWidget* widget, 196 GdkEventExpose* e) { 197 if (theme_service_ && theme_service_->UseGtkTheme()) 198 return FALSE; 199 return ((visible_mode_ == MODE_RELOAD) ? reload_ : stop_).OnExpose( 200 widget, e, hover_controller_.GetCurrentValue()); 201 } 202 203 gboolean ReloadButtonGtk::OnLeaveNotify(GtkWidget* /* widget */, 204 GdkEventCrossing* /* event */) { 205 ChangeMode(intended_mode_, true); 206 return FALSE; 207 } 208 209 gboolean ReloadButtonGtk::OnQueryTooltip(GtkWidget* /* sender */, 210 gint /* x */, 211 gint /* y */, 212 gboolean /* keyboard_mode */, 213 GtkTooltip* tooltip) { 214 // |location_bar_| can be NULL in tests. 215 if (!location_bar_) 216 return FALSE; 217 218 gtk_tooltip_set_text(tooltip, l10n_util::GetStringUTF8( 219 (visible_mode_ == MODE_RELOAD) ? 220 IDS_TOOLTIP_RELOAD : IDS_TOOLTIP_STOP).c_str()); 221 return TRUE; 222 } 223 224 void ReloadButtonGtk::UpdateThemeButtons() { 225 bool use_gtk = theme_service_ && theme_service_->UseGtkTheme(); 226 227 if (use_gtk) { 228 gtk_widget_ensure_style(widget()); 229 GtkIconSet* icon_set = gtk_style_lookup_icon_set( 230 widget()->style, 231 (visible_mode_ == MODE_RELOAD) ? GTK_STOCK_REFRESH : GTK_STOCK_STOP); 232 if (icon_set) { 233 GtkStateType state = static_cast<GtkStateType>( 234 GTK_WIDGET_STATE(widget())); 235 if (visible_mode_ == MODE_STOP && stop_.paint_override() != -1) 236 state = static_cast<GtkStateType>(stop_.paint_override()); 237 238 GdkPixbuf* pixbuf = gtk_icon_set_render_icon( 239 icon_set, 240 widget()->style, 241 gtk_widget_get_direction(widget()), 242 state, 243 GTK_ICON_SIZE_SMALL_TOOLBAR, 244 widget(), 245 NULL); 246 247 gtk_button_set_image(GTK_BUTTON(widget()), 248 gtk_image_new_from_pixbuf(pixbuf)); 249 g_object_unref(pixbuf); 250 } 251 252 gtk_widget_set_size_request(widget(), -1, -1); 253 GtkRequisition req; 254 gtk_widget_size_request(widget(), &req); 255 GtkButtonWidth = std::max(GtkButtonWidth, req.width); 256 gtk_widget_set_size_request(widget(), GtkButtonWidth, -1); 257 258 gtk_widget_set_app_paintable(widget(), FALSE); 259 gtk_widget_set_double_buffered(widget(), TRUE); 260 } else { 261 gtk_button_set_image(GTK_BUTTON(widget()), NULL); 262 263 gtk_widget_set_size_request(widget(), reload_.Width(), reload_.Height()); 264 265 gtk_widget_set_app_paintable(widget(), TRUE); 266 // We effectively double-buffer by virtue of having only one image... 267 gtk_widget_set_double_buffered(widget(), FALSE); 268 } 269 270 gtk_chrome_button_set_use_gtk_rendering(GTK_CHROME_BUTTON(widget()), use_gtk); 271 } 272 273 void ReloadButtonGtk::OnDoubleClickTimer() { 274 ChangeMode(intended_mode_, false); 275 } 276 277 void ReloadButtonGtk::OnStopToReloadTimer() { 278 ChangeMode(intended_mode_, true); 279 } 280