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
ReloadButtonGtk(LocationBarViewGtk * location_bar,Browser * browser)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
~ReloadButtonGtk()75 ReloadButtonGtk::~ReloadButtonGtk() {
76 widget_.Destroy();
77 }
78
ChangeMode(Mode mode,bool force)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
Observe(NotificationType type,const NotificationSource & source,const NotificationDetails &)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
OnClicked(GtkWidget *)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
OnExpose(GtkWidget * widget,GdkEventExpose * e)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
OnLeaveNotify(GtkWidget *,GdkEventCrossing *)203 gboolean ReloadButtonGtk::OnLeaveNotify(GtkWidget* /* widget */,
204 GdkEventCrossing* /* event */) {
205 ChangeMode(intended_mode_, true);
206 return FALSE;
207 }
208
OnQueryTooltip(GtkWidget *,gint,gint,gboolean,GtkTooltip * tooltip)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
UpdateThemeButtons()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
OnDoubleClickTimer()273 void ReloadButtonGtk::OnDoubleClickTimer() {
274 ChangeMode(intended_mode_, false);
275 }
276
OnStopToReloadTimer()277 void ReloadButtonGtk::OnStopToReloadTimer() {
278 ChangeMode(intended_mode_, true);
279 }
280