• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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