OLD | NEW |
| (Empty) |
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 | |
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 | |
109 ReloadButtonGtk::~ReloadButtonGtk() { | |
110 widget_.Destroy(); | |
111 } | |
112 | |
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 | |
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 | |
181 void ReloadButtonGtk::StoppedShowing() { | |
182 menu_visible_ = false; | |
183 ChangeMode(intended_mode_, true); | |
184 } | |
185 | |
186 //////////////////////////////////////////////////////////////////////////////// | |
187 // ReloadButtonGtk, SimpleMenuModel::Delegate implementation: | |
188 | |
189 bool ReloadButtonGtk::IsCommandIdChecked(int command_id) const { | |
190 return false; | |
191 } | |
192 | |
193 bool ReloadButtonGtk::IsCommandIdEnabled(int command_id) const { | |
194 return true; | |
195 } | |
196 | |
197 bool ReloadButtonGtk::IsCommandIdVisible(int command_id) const { | |
198 return true; | |
199 } | |
200 | |
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 | |
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 | |
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 | |
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 | |
279 gboolean ReloadButtonGtk::OnLeaveNotify(GtkWidget* /* widget */, | |
280 GdkEventCrossing* /* event */) { | |
281 ChangeMode(intended_mode_, true); | |
282 return FALSE; | |
283 } | |
284 | |
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 | |
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 | |
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 | |
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 | |
392 void ReloadButtonGtk::OnDoubleClickTimer() { | |
393 ChangeMode(intended_mode_, false); | |
394 } | |
395 | |
396 void ReloadButtonGtk::OnStopToReloadTimer() { | |
397 ChangeMode(intended_mode_, true); | |
398 } | |
399 | |
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 | |
413 void ReloadButtonGtk::DoReload(int command) { | |
414 // Start a timer - while this timer is running, the reload button cannot be | |
415 // changed to a stop button. We do not set |intended_mode_| to MODE_STOP | |
416 // here as the browser will do that when it actually starts loading (which | |
417 // may happen synchronously, thus the need to do this before telling the | |
418 // browser to execute the reload command). | |
419 double_click_timer_.Start(FROM_HERE, double_click_timer_delay_, this, | |
420 &ReloadButtonGtk::OnDoubleClickTimer); | |
421 | |
422 if (browser_) { | |
423 // Shift-clicking or Ctrl-clicking the reload button means we should ignore | |
424 // any cached content. | |
425 GdkModifierType modifier_state; | |
426 gtk_get_current_event_state(&modifier_state); | |
427 guint modifier_state_uint = modifier_state; | |
428 | |
429 // Default reload behaviour. | |
430 if (command == 0) { | |
431 if (modifier_state_uint & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) { | |
432 command = IDC_RELOAD_IGNORING_CACHE; | |
433 // Mask off Shift and Control so they don't affect the disposition | |
434 // below. | |
435 modifier_state_uint &= ~(GDK_SHIFT_MASK | GDK_CONTROL_MASK); | |
436 } else { | |
437 command = IDC_RELOAD; | |
438 } | |
439 } | |
440 | |
441 chrome::ExecuteCommandWithDisposition( | |
442 browser_, command, | |
443 event_utils::DispositionFromGdkState(modifier_state_uint)); | |
444 } | |
445 ++testing_reload_count_; | |
446 } | |
447 | |
448 bool ReloadButtonGtk::ReloadMenuEnabled() { | |
449 return browser_ && chrome::IsDebuggerAttachedToCurrentTab(browser_); | |
450 } | |
451 | |
452 void ReloadButtonGtk::ClearCache() { | |
453 if (browser_) | |
454 chrome::ClearCache(browser_); | |
455 } | |
OLD | NEW |