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.
30static int GtkButtonWidth = 0;
31
32// The time in milliseconds between when the user clicks and the menu appears.
33static const int kReloadMenuTimerDelay = 500;
34
35// Content of the Reload drop-down menu.
36static 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
45ReloadButtonGtk::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
109ReloadButtonGtk::~ReloadButtonGtk() {
110  widget_.Destroy();
111}
112
113void 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
166void 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
181void ReloadButtonGtk::StoppedShowing() {
182  menu_visible_ = false;
183  ChangeMode(intended_mode_, true);
184}
185
186////////////////////////////////////////////////////////////////////////////////
187// ReloadButtonGtk, SimpleMenuModel::Delegate implementation:
188
189bool ReloadButtonGtk::IsCommandIdChecked(int command_id) const {
190  return false;
191}
192
193bool ReloadButtonGtk::IsCommandIdEnabled(int command_id) const {
194  return true;
195}
196
197bool ReloadButtonGtk::IsCommandIdVisible(int command_id) const {
198  return true;
199}
200
201bool 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
231void 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
251void 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
270gboolean 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
279gboolean ReloadButtonGtk::OnLeaveNotify(GtkWidget* /* widget */,
280                                        GdkEventCrossing* /* event */) {
281  ChangeMode(intended_mode_, true);
282  return FALSE;
283}
284
285gboolean 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
302gboolean 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
324gboolean 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
343void 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
392void ReloadButtonGtk::OnDoubleClickTimer() {
393  ChangeMode(intended_mode_, false);
394}
395
396void ReloadButtonGtk::OnStopToReloadTimer() {
397  ChangeMode(intended_mode_, true);
398}
399
400void 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
413void 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
453bool ReloadButtonGtk::ReloadMenuEnabled() {
454  if (!browser_)
455    return false;
456  return chrome::IsDebuggerAttachedToCurrentTab(browser_);
457}
458
459void ReloadButtonGtk::ClearCache() {
460  if (browser_)
461    chrome::ClearCache(browser_);
462}
463