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.
30 static int GtkButtonWidth = 0;
31
32 // The time in milliseconds between when the user clicks and the menu appears.
33 static const int kReloadMenuTimerDelay = 500;
34
35 // Content of the Reload drop-down menu.
36 static 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
ReloadButtonGtk(LocationBarViewGtk * location_bar,Browser * browser)45 ReloadButtonGtk::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
~ReloadButtonGtk()109 ReloadButtonGtk::~ReloadButtonGtk() {
110 widget_.Destroy();
111 }
112
ChangeMode(Mode mode,bool force)113 void 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
Observe(int type,const content::NotificationSource & source,const content::NotificationDetails & details)166 void 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
StoppedShowing()181 void ReloadButtonGtk::StoppedShowing() {
182 menu_visible_ = false;
183 ChangeMode(intended_mode_, true);
184 }
185
186 ////////////////////////////////////////////////////////////////////////////////
187 // ReloadButtonGtk, SimpleMenuModel::Delegate implementation:
188
IsCommandIdChecked(int command_id) const189 bool ReloadButtonGtk::IsCommandIdChecked(int command_id) const {
190 return false;
191 }
192
IsCommandIdEnabled(int command_id) const193 bool ReloadButtonGtk::IsCommandIdEnabled(int command_id) const {
194 return true;
195 }
196
IsCommandIdVisible(int command_id) const197 bool ReloadButtonGtk::IsCommandIdVisible(int command_id) const {
198 return true;
199 }
200
GetAcceleratorForCommandId(int command_id,ui::Accelerator * out_accelerator)201 bool 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
ExecuteCommand(int command_id,int event_flags)231 void 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
OnClicked(GtkWidget *)251 void 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
OnExpose(GtkWidget * widget,GdkEventExpose * e)270 gboolean 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
OnLeaveNotify(GtkWidget *,GdkEventCrossing *)279 gboolean ReloadButtonGtk::OnLeaveNotify(GtkWidget* /* widget */,
280 GdkEventCrossing* /* event */) {
281 ChangeMode(intended_mode_, true);
282 return FALSE;
283 }
284
OnQueryTooltip(GtkWidget *,gint,gint,gboolean,GtkTooltip * tooltip)285 gboolean 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
OnButtonPress(GtkWidget * widget,GdkEventButton * event)302 gboolean 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
OnMouseMove(GtkWidget * widget,GdkEventMotion * event)324 gboolean 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
UpdateThemeButtons()343 void 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
OnDoubleClickTimer()392 void ReloadButtonGtk::OnDoubleClickTimer() {
393 ChangeMode(intended_mode_, false);
394 }
395
OnStopToReloadTimer()396 void ReloadButtonGtk::OnStopToReloadTimer() {
397 ChangeMode(intended_mode_, true);
398 }
399
ShowReloadMenu(int button,guint32 event_time)400 void 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
DoReload(int command)413 void 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
ReloadMenuEnabled()453 bool ReloadButtonGtk::ReloadMenuEnabled() {
454 if (!browser_)
455 return false;
456 return chrome::IsDebuggerAttachedToCurrentTab(browser_);
457 }
458
ClearCache()459 void ReloadButtonGtk::ClearCache() {
460 if (browser_)
461 chrome::ClearCache(browser_);
462 }
463