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.
5 #include "chrome/browser/ui/gtk/reload_button_gtk.h"
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"
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;
32 // The time in milliseconds between when the user clicks and the menu appears.
33 static const int kReloadMenuTimerDelay
= 500;
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
,
42 ////////////////////////////////////////////////////////////////////////////////
43 // ReloadButton, public:
45 ReloadButtonGtk::ReloadButtonGtk(LocationBarViewGtk
* location_bar
,
47 : location_bar_(location_bar
),
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)),
58 testing_mouse_hovered_(false),
59 testing_reload_count_(0),
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
]);
66 gtk_widget_set_size_request(widget(), reload_
.Width(), reload_
.Height());
68 gtk_widget_set_app_paintable(widget(), TRUE
);
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
);
76 gtk_widget_set_has_tooltip(widget(), TRUE
);
77 g_signal_connect(widget(), "query-tooltip", G_CALLBACK(OnQueryTooltipThunk
),
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);
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));
91 hover_controller_
.Init(widget());
92 gtk_util::SetButtonTriggersNavigation(widget());
95 theme_service_
->InitThemesFor(this);
97 chrome::NOTIFICATION_BROWSER_THEME_CHANGED
,
98 content::Source
<ThemeService
>(theme_service_
));
101 // Set the default double-click timer delay to the system double-click time.
103 GtkSettings
* settings
= gtk_settings_get_default();
104 g_object_get(G_OBJECT(settings
), "gtk-double-click-time", &timer_delay_ms
,
106 double_click_timer_delay_
= base::TimeDelta::FromMilliseconds(timer_delay_ms
);
109 ReloadButtonGtk::~ReloadButtonGtk() {
113 void ReloadButtonGtk::ChangeMode(Mode mode
, bool force
) {
114 intended_mode_
= mode
;
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
;
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()));
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
);
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
);
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();
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
);
163 ////////////////////////////////////////////////////////////////////////////////
164 // ReloadButtonGtk, content::NotificationObserver implementation:
166 void ReloadButtonGtk::Observe(int type
,
167 const content::NotificationSource
& source
,
168 const content::NotificationDetails
& details
) {
169 DCHECK(chrome::NOTIFICATION_BROWSER_THEME_CHANGED
== type
);
171 GtkThemeService
* provider
= static_cast<GtkThemeService
*>(
172 content::Source
<ThemeService
>(source
).ptr());
173 DCHECK_EQ(provider
, theme_service_
);
175 UpdateThemeButtons();
178 ////////////////////////////////////////////////////////////////////////////////
179 // ReloadButtonGtk, MenuGtk::Delegate implementation:
181 void ReloadButtonGtk::StoppedShowing() {
182 menu_visible_
= false;
183 ChangeMode(intended_mode_
, true);
186 ////////////////////////////////////////////////////////////////////////////////
187 // ReloadButtonGtk, SimpleMenuModel::Delegate implementation:
189 bool ReloadButtonGtk::IsCommandIdChecked(int command_id
) const {
193 bool ReloadButtonGtk::IsCommandIdEnabled(int command_id
) const {
197 bool ReloadButtonGtk::IsCommandIdVisible(int command_id
) const {
201 bool ReloadButtonGtk::GetAcceleratorForCommandId(
203 ui::Accelerator
* out_accelerator
) {
205 switch (command_id
) {
206 case IDS_RELOAD_MENU_NORMAL_RELOAD_ITEM
:
207 command
= IDC_RELOAD
;
209 case IDS_RELOAD_MENU_HARD_RELOAD_ITEM
:
210 command
= IDC_RELOAD_IGNORING_CACHE
;
212 case IDS_RELOAD_MENU_EMPTY_AND_HARD_RELOAD_ITEM
:
216 LOG(ERROR
) << "Unknown reload menu command";
220 const ui::Accelerator
* accelerator
=
221 AcceleratorsGtk::GetInstance()->
222 GetPrimaryAcceleratorForCommand(command
);
224 *out_accelerator
= *accelerator
;
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
);
236 case IDS_RELOAD_MENU_HARD_RELOAD_ITEM
:
237 DoReload(IDC_RELOAD_IGNORING_CACHE
);
239 case IDS_RELOAD_MENU_EMPTY_AND_HARD_RELOAD_ITEM
:
241 DoReload(IDC_RELOAD_IGNORING_CACHE
);
244 LOG(ERROR
) << "Unknown reload menu command";
248 ////////////////////////////////////////////////////////////////////////////////
249 // ReloadButtonGtk, private:
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
)
260 chrome::Stop(browser_
);
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()) {
270 gboolean
ReloadButtonGtk::OnExpose(GtkWidget
* widget
,
272 TRACE_EVENT0("ui::gtk", "ReloadButtonGtk::OnExpose");
273 if (theme_service_
&& theme_service_
->UsingNativeTheme())
275 return ((visible_mode_
== MODE_RELOAD
) ? reload_
: stop_
).OnExpose(
276 widget
, e
, hover_controller_
.GetCurrentValue());
279 gboolean
ReloadButtonGtk::OnLeaveNotify(GtkWidget
* /* widget */,
280 GdkEventCrossing
* /* event */) {
281 ChangeMode(intended_mode_
, true);
285 gboolean
ReloadButtonGtk::OnQueryTooltip(GtkWidget
* /* sender */,
288 gboolean
/* keyboard_mode */,
289 GtkTooltip
* tooltip
) {
290 // |location_bar_| can be NULL in tests.
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());
302 gboolean
ReloadButtonGtk::OnButtonPress(GtkWidget
* widget
,
303 GdkEventButton
* event
) {
304 if (!ReloadMenuEnabled() || visible_mode_
== MODE_STOP
)
307 if (event
->button
== 3)
308 ShowReloadMenu(event
->button
, event
->time
);
310 if (event
->button
!= 1)
313 y_position_of_last_press_
= static_cast<int>(event
->y
);
314 base::MessageLoop::current()->PostDelayedTask(
316 base::Bind(&ReloadButtonGtk::ShowReloadMenu
,
317 weak_factory_
.GetWeakPtr(),
320 base::TimeDelta::FromMilliseconds(kReloadMenuTimerDelay
));
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())
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
)
337 // We will show the menu now. Cancel the delayed event.
338 weak_factory_
.InvalidateWeakPtrs();
339 ShowReloadMenu(/* button */ 1, event
->time
);
343 void ReloadButtonGtk::UpdateThemeButtons() {
344 bool use_gtk
= theme_service_
&& theme_service_
->UsingNativeTheme();
347 gtk_widget_ensure_style(widget());
348 GtkStyle
* style
= gtk_widget_get_style(widget());
349 GtkIconSet
* icon_set
= gtk_style_lookup_icon_set(
351 (visible_mode_
== MODE_RELOAD
) ? GTK_STOCK_REFRESH
: GTK_STOCK_STOP
);
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());
357 GdkPixbuf
* pixbuf
= gtk_icon_set_render_icon(
360 gtk_widget_get_direction(widget()),
362 GTK_ICON_SIZE_SMALL_TOOLBAR
,
366 gtk_button_set_image(GTK_BUTTON(widget()),
367 gtk_image_new_from_pixbuf(pixbuf
));
368 g_object_unref(pixbuf
);
371 gtk_widget_set_size_request(widget(), -1, -1);
373 gtk_widget_size_request(widget(), &req
);
374 GtkButtonWidth
= std::max(GtkButtonWidth
, req
.width
);
375 gtk_widget_set_size_request(widget(), GtkButtonWidth
, -1);
377 gtk_widget_set_app_paintable(widget(), FALSE
);
378 gtk_widget_set_double_buffered(widget(), TRUE
);
380 gtk_button_set_image(GTK_BUTTON(widget()), NULL
);
382 gtk_widget_set_size_request(widget(), reload_
.Width(), reload_
.Height());
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
);
389 gtk_chrome_button_set_use_gtk_rendering(GTK_CHROME_BUTTON(widget()), use_gtk
);
392 void ReloadButtonGtk::OnDoubleClickTimer() {
393 ChangeMode(intended_mode_
, false);
396 void ReloadButtonGtk::OnStopToReloadTimer() {
397 ChangeMode(intended_mode_
, true);
400 void ReloadButtonGtk::ShowReloadMenu(int button
, guint32 event_time
) {
401 if (!ReloadMenuEnabled() || visible_mode_
== MODE_STOP
)
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()),
409 gtk_widget_queue_draw(widget());
410 menu_
->PopupForWidget(widget(), button
, event_time
);
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
;
420 // Default reload behaviour.
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
);
427 command
= IDC_RELOAD
;
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
437 location_bar_
->Revert();
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
);
449 chrome::ExecuteCommandWithDisposition(browser_
, command
, disposition
);
450 ++testing_reload_count_
;
453 bool ReloadButtonGtk::ReloadMenuEnabled() {
456 return chrome::IsDebuggerAttachedToCurrentTab(browser_
);
459 void ReloadButtonGtk::ClearCache() {
461 chrome::ClearCache(browser_
);