- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / gtk / reload_button_gtk.cc
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   // 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
453 bool ReloadButtonGtk::ReloadMenuEnabled() {
454   if (!browser_)
455     return false;
456   return chrome::IsDebuggerAttachedToCurrentTab(browser_);
457 }
458
459 void ReloadButtonGtk::ClearCache() {
460   if (browser_)
461     chrome::ClearCache(browser_);
462 }