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/browser_titlebar.h"
6
7 #include <gdk/gdkkeysyms.h>
8 #include <gtk/gtk.h>
9
10 #include <string>
11 #include <vector>
12
13 #include "base/command_line.h"
14 #include "base/memory/singleton.h"
15 #include "base/string_piece.h"
16 #include "base/string_tokenizer.h"
17 #include "base/utf_string_conversions.h"
18 #include "chrome/app/chrome_command_ids.h"
19 #include "chrome/browser/prefs/pref_service.h"
20 #include "chrome/browser/profiles/profile.h"
21 #include "chrome/browser/ui/browser.h"
22 #include "chrome/browser/ui/gtk/accelerators_gtk.h"
23 #include "chrome/browser/ui/gtk/browser_window_gtk.h"
24 #include "chrome/browser/ui/gtk/custom_button.h"
25 #if defined(USE_GCONF)
26 #include "chrome/browser/ui/gtk/gconf_titlebar_listener.h"
27 #endif
28 #include "chrome/browser/ui/gtk/gtk_theme_service.h"
29 #include "chrome/browser/ui/gtk/gtk_util.h"
30 #include "chrome/browser/ui/gtk/menu_gtk.h"
31 #include "chrome/browser/ui/gtk/nine_box.h"
32 #include "chrome/browser/ui/gtk/tabs/tab_strip_gtk.h"
33 #include "chrome/browser/ui/toolbar/encoding_menu_controller.h"
34 #include "chrome/browser/ui/toolbar/wrench_menu_model.h"
35 #include "chrome/common/pref_names.h"
36 #include "content/browser/tab_contents/tab_contents.h"
37 #include "content/common/notification_service.h"
38 #include "grit/app_resources.h"
39 #include "grit/generated_resources.h"
40 #include "grit/theme_resources.h"
41 #include "ui/base/l10n/l10n_util.h"
42 #include "ui/base/resource/resource_bundle.h"
43 #include "ui/gfx/gtk_util.h"
44 #include "ui/gfx/skbitmap_operations.h"
45
46 namespace {
47
48 // The space above the titlebars.
49 const int kTitlebarHeight = 14;
50
51 // The thickness in pixels of the tab border.
52 const int kTabOuterThickness = 1;
53
54 // Amount to offset the tab images relative to the background.
55 const int kNormalVerticalOffset = kTitlebarHeight + kTabOuterThickness;
56
57 // A linux specific menu item for toggling window decorations.
58 const int kShowWindowDecorationsCommand = 200;
59
60 const int kOTRBottomSpacing = 1;
61 // There are 2 px on each side of the OTR avatar (between the frame border and
62 // it on the left, and between it and the tabstrip on the right).
63 const int kOTRSideSpacing = 2;
64
65 // The thickness of the custom frame border; we need it here to enlarge the
66 // close button whent the custom frame border isn't showing but the custom
67 // titlebar is showing.
68 const int kFrameBorderThickness = 4;
69
70 // There is a 4px gap between the icon and the title text.
71 const int kIconTitleSpacing = 4;
72
73 // Padding around the icon when in app mode or popup mode.
74 const int kAppModePaddingTop = 5;
75 const int kAppModePaddingBottom = 4;
76 const int kAppModePaddingLeft = 2;
77
78 // The left padding of the tab strip. In Views, the tab strip has a left
79 // margin of FrameBorderThickness + kClientEdgeThickness. This offset is to
80 // account for kClientEdgeThickness.
81 const int kTabStripLeftPadding = 1;
82
83 // Spacing between buttons of the titlebar.
84 const int kButtonSpacing = 2;
85
86 // Spacing around outside of titlebar buttons.
87 const int kButtonOuterPadding = 2;
88
89 // Spacing between tabstrip and window control buttons (when the window is
90 // maximized).
91 const int kMaximizedTabstripPadding = 16;
92
OnMouseMoveEvent(GtkWidget * widget,GdkEventMotion * event,BrowserWindowGtk * browser_window)93 gboolean OnMouseMoveEvent(GtkWidget* widget, GdkEventMotion* event,
94 BrowserWindowGtk* browser_window) {
95 // Reset to the default mouse cursor.
96 browser_window->ResetCustomFrameCursor();
97 return TRUE;
98 }
99
GetOTRAvatar()100 GdkPixbuf* GetOTRAvatar() {
101 static GdkPixbuf* otr_avatar = NULL;
102 if (!otr_avatar) {
103 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
104 otr_avatar = rb.GetRTLEnabledPixbufNamed(IDR_OTR_ICON);
105 }
106 return otr_avatar;
107 }
108
109 // Converts a GdkColor to a color_utils::HSL.
GdkColorToHSL(const GdkColor * color)110 color_utils::HSL GdkColorToHSL(const GdkColor* color) {
111 color_utils::HSL hsl;
112 color_utils::SkColorToHSL(SkColorSetRGB(color->red >> 8,
113 color->green >> 8,
114 color->blue >> 8), &hsl);
115 return hsl;
116 }
117
118 // Returns either |one| or |two| based on which has a greater difference in
119 // luminosity.
PickLuminosityContrastingColor(const GdkColor * base,const GdkColor * one,const GdkColor * two)120 GdkColor PickLuminosityContrastingColor(const GdkColor* base,
121 const GdkColor* one,
122 const GdkColor* two) {
123 // Convert all GdkColors to color_utils::HSLs.
124 color_utils::HSL baseHSL = GdkColorToHSL(base);
125 color_utils::HSL oneHSL = GdkColorToHSL(one);
126 color_utils::HSL twoHSL = GdkColorToHSL(two);
127 double one_difference = fabs(baseHSL.l - oneHSL.l);
128 double two_difference = fabs(baseHSL.l - twoHSL.l);
129
130 // Be biased towards the first color presented.
131 if (two_difference > one_difference + 0.1)
132 return *two;
133 else
134 return *one;
135 }
136
137 } // namespace
138
139 ////////////////////////////////////////////////////////////////////////////////
140 // PopupPageMenuModel
141
142 // A menu model that builds the contents of the menu shown for popups (when the
143 // user clicks on the favicon) and all of its submenus.
144 class PopupPageMenuModel : public ui::SimpleMenuModel {
145 public:
146 explicit PopupPageMenuModel(ui::SimpleMenuModel::Delegate* delegate,
147 Browser* browser);
~PopupPageMenuModel()148 virtual ~PopupPageMenuModel() { }
149
150 private:
151 void Build();
152
153 // Models for submenus referenced by this model. SimpleMenuModel only uses
154 // weak references so these must be kept for the lifetime of the top-level
155 // model.
156 scoped_ptr<ZoomMenuModel> zoom_menu_model_;
157 scoped_ptr<EncodingMenuModel> encoding_menu_model_;
158 Browser* browser_; // weak
159
160 DISALLOW_COPY_AND_ASSIGN(PopupPageMenuModel);
161 };
162
PopupPageMenuModel(ui::SimpleMenuModel::Delegate * delegate,Browser * browser)163 PopupPageMenuModel::PopupPageMenuModel(
164 ui::SimpleMenuModel::Delegate* delegate,
165 Browser* browser)
166 : ui::SimpleMenuModel(delegate), browser_(browser) {
167 Build();
168 }
169
Build()170 void PopupPageMenuModel::Build() {
171 AddItemWithStringId(IDC_BACK, IDS_CONTENT_CONTEXT_BACK);
172 AddItemWithStringId(IDC_FORWARD, IDS_CONTENT_CONTEXT_FORWARD);
173 AddItemWithStringId(IDC_RELOAD, IDS_APP_MENU_RELOAD);
174 AddSeparator();
175 AddItemWithStringId(IDC_SHOW_AS_TAB, IDS_SHOW_AS_TAB);
176 AddItemWithStringId(IDC_COPY_URL, IDS_APP_MENU_COPY_URL);
177 AddSeparator();
178 AddItemWithStringId(IDC_CUT, IDS_CUT);
179 AddItemWithStringId(IDC_COPY, IDS_COPY);
180 AddItemWithStringId(IDC_PASTE, IDS_PASTE);
181 AddSeparator();
182 AddItemWithStringId(IDC_FIND, IDS_FIND);
183 AddItemWithStringId(IDC_PRINT, IDS_PRINT);
184 zoom_menu_model_.reset(new ZoomMenuModel(delegate()));
185 AddSubMenuWithStringId(IDC_ZOOM_MENU, IDS_ZOOM_MENU, zoom_menu_model_.get());
186
187 encoding_menu_model_.reset(new EncodingMenuModel(browser_));
188 AddSubMenuWithStringId(IDC_ENCODING_MENU, IDS_ENCODING_MENU,
189 encoding_menu_model_.get());
190
191 AddSeparator();
192 AddItemWithStringId(IDC_CLOSE_WINDOW, IDS_CLOSE);
193 }
194
195 ////////////////////////////////////////////////////////////////////////////////
196 // BrowserTitlebar
197
198 // static
199 const char BrowserTitlebar::kDefaultButtonString[] = ":minimize,maximize,close";
200
BrowserTitlebar(BrowserWindowGtk * browser_window,GtkWindow * window)201 BrowserTitlebar::BrowserTitlebar(BrowserWindowGtk* browser_window,
202 GtkWindow* window)
203 : browser_window_(browser_window),
204 window_(window),
205 titlebar_left_buttons_vbox_(NULL),
206 titlebar_right_buttons_vbox_(NULL),
207 titlebar_left_buttons_hbox_(NULL),
208 titlebar_right_buttons_hbox_(NULL),
209 titlebar_left_spy_frame_(NULL),
210 titlebar_right_spy_frame_(NULL),
211 top_padding_left_(NULL),
212 top_padding_right_(NULL),
213 app_mode_favicon_(NULL),
214 app_mode_title_(NULL),
215 using_custom_frame_(false),
216 window_has_focus_(false),
217 theme_service_(NULL) {
218 Init();
219 }
220
Init()221 void BrowserTitlebar::Init() {
222 // The widget hierarchy is shown below.
223 //
224 // +- EventBox (container_) ------------------------------------------------+
225 // +- HBox (container_hbox_) -----------------------------------------------+
226 // |+ VBox ---++- Algn. -++- Alignment --------------++- Algn. -++ VBox ---+|
227 // || titlebar||titlebar || (titlebar_alignment_) ||titlebar || titlebar||
228 // || left ||left || ||right || right ||
229 // || button ||spy || ||spy || button ||
230 // || vbox ||frame ||+- TabStripGtk ---------+||frame || vbox ||
231 // || || || tab tab tabclose ||| || ||
232 // || || ||+------------------------+|| || ||
233 // |+---------++---------++--------------------------++---------++---------+|
234 // +------------------------------------------------------------------------+
235 //
236 // There are two vboxes on either side of |container_hbox_| because when the
237 // desktop is GNOME, the button placement is configurable based on a metacity
238 // gconf key. We can't just have one vbox and move it back and forth because
239 // the gconf language allows you to place buttons on both sides of the
240 // window. This being Linux, I'm sure there's a bunch of people who have
241 // already configured their window manager to do so and we don't want to get
242 // confused when that happens. The actual contents of these vboxes are lazily
243 // generated so they don't interfere with alignment when everything is
244 // stuffed in the other box.
245 //
246 // Each vbox has the contains the following hierarchy if it contains buttons:
247 //
248 // +- VBox (titlebar_{l,r}_buttons_vbox_) ---------+
249 // |+- Fixed (top_padding_{l,r}_) ----------------+|
250 // ||+- HBox (titlebar_{l,r}_buttons_hbox_ ------+||
251 // ||| (buttons of a configurable layout) |||
252 // ||+-------------------------------------------+||
253 // |+---------------------------------------------+|
254 // +-----------------------------------------------+
255 //
256 // The two spy alignments are only allocated if this window is an incognito
257 // window. Only one of them holds the spy image.
258 //
259 // If we're a popup window or in app mode, we don't display the spy guy or
260 // the tab strip. Instead, put an hbox in titlebar_alignment_ in place of
261 // the tab strip.
262 // +- Alignment (titlebar_alignment_) -----------------------------------+
263 // |+ HBox -------------------------------------------------------------+|
264 // ||+- TabStripGtk -++- Image ----------------++- Label --------------+||
265 // ||| hidden ++ (app_mode_favicon_) || (app_mode_title_) |||
266 // ||| || favicon || page title |||
267 // ||+---------------++------------------------++----------------------+||
268 // |+-------------------------------------------------------------------+|
269 // +---------------------------------------------------------------------+
270 container_hbox_ = gtk_hbox_new(FALSE, 0);
271
272 container_ = gtk_event_box_new();
273 gtk_widget_set_name(container_, "chrome-browser-titlebar");
274 gtk_event_box_set_visible_window(GTK_EVENT_BOX(container_), FALSE);
275 gtk_container_add(GTK_CONTAINER(container_), container_hbox_);
276
277 g_signal_connect(container_, "scroll-event", G_CALLBACK(OnScrollThunk), this);
278
279 g_signal_connect(window_, "window-state-event",
280 G_CALLBACK(OnWindowStateChangedThunk), this);
281
282 // Allocate the two button boxes on the left and right parts of the bar, and
283 // spyguy frames in case of incognito mode.
284 titlebar_left_buttons_vbox_ = gtk_vbox_new(FALSE, 0);
285 gtk_box_pack_start(GTK_BOX(container_hbox_), titlebar_left_buttons_vbox_,
286 FALSE, FALSE, 0);
287 if (browser_window_->browser()->profile()->IsOffTheRecord() &&
288 browser_window_->browser()->type() == Browser::TYPE_NORMAL) {
289 titlebar_left_spy_frame_ = gtk_alignment_new(0.0, 0.0, 1.0, 1.0);
290 gtk_widget_set_no_show_all(titlebar_left_spy_frame_, TRUE);
291 gtk_alignment_set_padding(GTK_ALIGNMENT(titlebar_left_spy_frame_), 0,
292 kOTRBottomSpacing, kOTRSideSpacing, kOTRSideSpacing);
293 gtk_box_pack_start(GTK_BOX(container_hbox_), titlebar_left_spy_frame_,
294 FALSE, FALSE, 0);
295
296 titlebar_right_spy_frame_ = gtk_alignment_new(0.0, 0.0, 1.0, 1.0);
297 gtk_widget_set_no_show_all(titlebar_right_spy_frame_, TRUE);
298 gtk_alignment_set_padding(GTK_ALIGNMENT(titlebar_right_spy_frame_), 0,
299 kOTRBottomSpacing, kOTRSideSpacing, kOTRSideSpacing);
300 gtk_box_pack_end(GTK_BOX(container_hbox_), titlebar_right_spy_frame_,
301 FALSE, FALSE, 0);
302 }
303 titlebar_right_buttons_vbox_ = gtk_vbox_new(FALSE, 0);
304 gtk_box_pack_end(GTK_BOX(container_hbox_), titlebar_right_buttons_vbox_,
305 FALSE, FALSE, 0);
306
307 #if defined(USE_GCONF)
308 // Either read the gconf database and register for updates (on GNOME), or use
309 // the default value (anywhere else).
310 GConfTitlebarListener::GetInstance()->SetTitlebarButtons(this);
311 #else
312 BuildButtons(kDefaultButtonString);
313 #endif
314
315 // We use an alignment to control the titlebar height.
316 titlebar_alignment_ = gtk_alignment_new(0.0, 0.0, 1.0, 1.0);
317 if (browser_window_->browser()->type() == Browser::TYPE_NORMAL) {
318 gtk_box_pack_start(GTK_BOX(container_hbox_), titlebar_alignment_, TRUE,
319 TRUE, 0);
320
321 // Put the tab strip in the titlebar.
322 gtk_container_add(GTK_CONTAINER(titlebar_alignment_),
323 browser_window_->tabstrip()->widget());
324 } else {
325 // App mode specific widgets.
326 gtk_box_pack_start(GTK_BOX(container_hbox_), titlebar_alignment_, TRUE,
327 TRUE, 0);
328 GtkWidget* app_mode_hbox = gtk_hbox_new(FALSE, kIconTitleSpacing);
329 gtk_container_add(GTK_CONTAINER(titlebar_alignment_), app_mode_hbox);
330
331 // Put the tab strip in the hbox even though we don't show it. Sometimes
332 // we need the position of the tab strip so make sure it's in our widget
333 // hierarchy.
334 gtk_box_pack_start(GTK_BOX(app_mode_hbox),
335 browser_window_->tabstrip()->widget(), FALSE, FALSE, 0);
336
337 GtkWidget* favicon_event_box = gtk_event_box_new();
338 gtk_event_box_set_visible_window(GTK_EVENT_BOX(favicon_event_box), FALSE);
339 g_signal_connect(favicon_event_box, "button-press-event",
340 G_CALLBACK(OnButtonPressedThunk), this);
341 gtk_box_pack_start(GTK_BOX(app_mode_hbox), favicon_event_box, FALSE,
342 FALSE, 0);
343 // We use the app logo as a placeholder image so the title doesn't jump
344 // around.
345 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
346 app_mode_favicon_ = gtk_image_new_from_pixbuf(
347 rb.GetRTLEnabledPixbufNamed(IDR_PRODUCT_LOGO_16));
348 g_object_set_data(G_OBJECT(app_mode_favicon_), "left-align-popup",
349 reinterpret_cast<void*>(true));
350 gtk_container_add(GTK_CONTAINER(favicon_event_box), app_mode_favicon_);
351
352 app_mode_title_ = gtk_label_new(NULL);
353 gtk_label_set_ellipsize(GTK_LABEL(app_mode_title_), PANGO_ELLIPSIZE_END);
354 gtk_misc_set_alignment(GTK_MISC(app_mode_title_), 0.0, 0.5);
355 gtk_box_pack_start(GTK_BOX(app_mode_hbox), app_mode_title_, TRUE, TRUE,
356 0);
357
358 // Register with the theme provider to set the |app_mode_title_| label
359 // color.
360 theme_service_ = GtkThemeService::GetFrom(
361 browser_window_->browser()->profile());
362 registrar_.Add(this, NotificationType::BROWSER_THEME_CHANGED,
363 NotificationService::AllSources());
364 theme_service_->InitThemesFor(this);
365 UpdateTitleAndIcon();
366 }
367
368 gtk_widget_show_all(container_);
369
370 ui::ActiveWindowWatcherX::AddObserver(this);
371 }
372
~BrowserTitlebar()373 BrowserTitlebar::~BrowserTitlebar() {
374 ui::ActiveWindowWatcherX::RemoveObserver(this);
375 #if defined(USE_GCONF)
376 GConfTitlebarListener::GetInstance()->RemoveObserver(this);
377 #endif
378 }
379
BuildButtons(const std::string & button_string)380 void BrowserTitlebar::BuildButtons(const std::string& button_string) {
381 // Clear out all previous data.
382 close_button_.reset();
383 restore_button_.reset();
384 maximize_button_.reset();
385 minimize_button_.reset();
386 gtk_util::RemoveAllChildren(titlebar_left_buttons_vbox_);
387 gtk_util::RemoveAllChildren(titlebar_right_buttons_vbox_);
388 titlebar_left_buttons_hbox_ = NULL;
389 titlebar_right_buttons_hbox_ = NULL;
390 top_padding_left_ = NULL;
391 top_padding_right_ = NULL;
392
393 bool left_side = true;
394 StringTokenizer tokenizer(button_string, ":,");
395 tokenizer.set_options(StringTokenizer::RETURN_DELIMS);
396 int left_count = 0;
397 int right_count = 0;
398 while (tokenizer.GetNext()) {
399 if (tokenizer.token_is_delim()) {
400 if (*tokenizer.token_begin() == ':')
401 left_side = false;
402 } else {
403 base::StringPiece token = tokenizer.token_piece();
404 if (token == "minimize") {
405 (left_side ? left_count : right_count)++;
406 GtkWidget* parent_box = GetButtonHBox(left_side);
407 minimize_button_.reset(
408 BuildTitlebarButton(IDR_MINIMIZE, IDR_MINIMIZE_P,
409 IDR_MINIMIZE_H, parent_box,
410 IDS_XPFRAME_MINIMIZE_TOOLTIP));
411
412 gtk_widget_size_request(minimize_button_->widget(),
413 &minimize_button_req_);
414 } else if (token == "maximize") {
415 (left_side ? left_count : right_count)++;
416 GtkWidget* parent_box = GetButtonHBox(left_side);
417 restore_button_.reset(
418 BuildTitlebarButton(IDR_RESTORE, IDR_RESTORE_P,
419 IDR_RESTORE_H, parent_box,
420 IDS_XPFRAME_RESTORE_TOOLTIP));
421 maximize_button_.reset(
422 BuildTitlebarButton(IDR_MAXIMIZE, IDR_MAXIMIZE_P,
423 IDR_MAXIMIZE_H, parent_box,
424 IDS_XPFRAME_MAXIMIZE_TOOLTIP));
425
426 gtk_util::SetButtonClickableByMouseButtons(maximize_button_->widget(),
427 true, true, true);
428 gtk_widget_size_request(restore_button_->widget(),
429 &restore_button_req_);
430 } else if (token == "close") {
431 (left_side ? left_count : right_count)++;
432 GtkWidget* parent_box = GetButtonHBox(left_side);
433 close_button_.reset(
434 BuildTitlebarButton(IDR_CLOSE, IDR_CLOSE_P,
435 IDR_CLOSE_H, parent_box,
436 IDS_XPFRAME_CLOSE_TOOLTIP));
437 close_button_->set_flipped(left_side);
438
439 gtk_widget_size_request(close_button_->widget(), &close_button_req_);
440 }
441 // Ignore any other values like "pin" since we don't have images for
442 // those.
443 }
444 }
445
446 // If we are in incognito mode, add the spy guy to either the end of the left
447 // or the beginning of the right depending on which side has fewer buttons.
448 if (browser_window_->browser()->profile()->IsOffTheRecord() &&
449 browser_window_->browser()->type() == Browser::TYPE_NORMAL) {
450 GtkWidget* spy_guy = gtk_image_new_from_pixbuf(GetOTRAvatar());
451 gtk_misc_set_alignment(GTK_MISC(spy_guy), 0.0, 1.0);
452 gtk_widget_set_size_request(spy_guy, -1, 0);
453 gtk_widget_show(spy_guy);
454
455 // Remove previous state.
456 gtk_util::RemoveAllChildren(titlebar_left_spy_frame_);
457 gtk_util::RemoveAllChildren(titlebar_right_spy_frame_);
458
459 if (right_count > left_count) {
460 gtk_container_add(GTK_CONTAINER(titlebar_left_spy_frame_), spy_guy);
461 gtk_widget_show(titlebar_left_spy_frame_);
462 gtk_widget_hide(titlebar_right_spy_frame_);
463 } else {
464 gtk_container_add(GTK_CONTAINER(titlebar_right_spy_frame_), spy_guy);
465 gtk_widget_show(titlebar_right_spy_frame_);
466 gtk_widget_hide(titlebar_left_spy_frame_);
467 }
468 }
469
470 // Now show the correct widgets in the two hierarchies.
471 if (using_custom_frame_) {
472 gtk_widget_show_all(titlebar_left_buttons_vbox_);
473 gtk_widget_show_all(titlebar_right_buttons_vbox_);
474 }
475 UpdateMaximizeRestoreVisibility();
476 }
477
GetButtonHBox(bool left_side)478 GtkWidget* BrowserTitlebar::GetButtonHBox(bool left_side) {
479 if (left_side && titlebar_left_buttons_hbox_)
480 return titlebar_left_buttons_hbox_;
481 else if (!left_side && titlebar_right_buttons_hbox_)
482 return titlebar_right_buttons_hbox_;
483
484 // We put the min/max/restore/close buttons in a vbox so they are top aligned
485 // (up to padding) and don't vertically stretch.
486 GtkWidget* vbox = left_side ? titlebar_left_buttons_vbox_ :
487 titlebar_right_buttons_vbox_;
488
489 GtkWidget* top_padding = gtk_fixed_new();
490 gtk_widget_set_size_request(top_padding, -1, kButtonOuterPadding);
491 gtk_box_pack_start(GTK_BOX(vbox), top_padding, FALSE, FALSE, 0);
492
493 GtkWidget* buttons_hbox = gtk_hbox_new(FALSE, kButtonSpacing);
494 gtk_box_pack_start(GTK_BOX(vbox), buttons_hbox, FALSE, FALSE, 0);
495
496 if (left_side) {
497 titlebar_left_buttons_hbox_ = buttons_hbox;
498 top_padding_left_ = top_padding;
499 } else {
500 titlebar_right_buttons_hbox_ = buttons_hbox;
501 top_padding_right_ = top_padding;
502 }
503
504 return buttons_hbox;
505 }
506
BuildTitlebarButton(int image,int image_pressed,int image_hot,GtkWidget * box,int tooltip)507 CustomDrawButton* BrowserTitlebar::BuildTitlebarButton(int image,
508 int image_pressed, int image_hot, GtkWidget* box, int tooltip) {
509 CustomDrawButton* button = new CustomDrawButton(image, image_pressed,
510 image_hot, 0);
511 gtk_widget_add_events(GTK_WIDGET(button->widget()), GDK_POINTER_MOTION_MASK);
512 g_signal_connect(button->widget(), "clicked",
513 G_CALLBACK(OnButtonClickedThunk), this);
514 g_signal_connect(button->widget(), "motion-notify-event",
515 G_CALLBACK(OnMouseMoveEvent), browser_window_);
516 std::string localized_tooltip = l10n_util::GetStringUTF8(tooltip);
517 gtk_widget_set_tooltip_text(button->widget(),
518 localized_tooltip.c_str());
519 gtk_box_pack_start(GTK_BOX(box), button->widget(), FALSE, FALSE, 0);
520 return button;
521 }
522
UpdateCustomFrame(bool use_custom_frame)523 void BrowserTitlebar::UpdateCustomFrame(bool use_custom_frame) {
524 using_custom_frame_ = use_custom_frame;
525 if (use_custom_frame) {
526 if (titlebar_left_buttons_vbox_)
527 gtk_widget_show_all(titlebar_left_buttons_vbox_);
528 if (titlebar_right_buttons_vbox_)
529 gtk_widget_show_all(titlebar_right_buttons_vbox_);
530 } else {
531 if (titlebar_left_buttons_vbox_)
532 gtk_widget_hide(titlebar_left_buttons_vbox_);
533 if (titlebar_right_buttons_vbox_)
534 gtk_widget_hide(titlebar_right_buttons_vbox_);
535 }
536 UpdateTitlebarAlignment();
537 }
538
UpdateTitleAndIcon()539 void BrowserTitlebar::UpdateTitleAndIcon() {
540 if (!app_mode_title_)
541 return;
542
543 // Get the page title and elide it to the available space.
544 string16 title = browser_window_->browser()->GetWindowTitleForCurrentTab();
545 gtk_label_set_text(GTK_LABEL(app_mode_title_), UTF16ToUTF8(title).c_str());
546
547 // Note: this isn't browser_window_->browser()->type() & Browser::TYPE_APP
548 // because we want to exclude Browser::TYPE_APP_POPUP.
549 if (browser_window_->browser()->type() == Browser::TYPE_APP ||
550 browser_window_->browser()->type() == Browser::TYPE_APP_PANEL) {
551 // Update the system app icon. We don't need to update the icon in the top
552 // left of the custom frame, that will get updated when the throbber is
553 // updated.
554 SkBitmap icon = browser_window_->browser()->GetCurrentPageIcon();
555 if (icon.empty()) {
556 gtk_util::SetWindowIcon(window_);
557 } else {
558 GdkPixbuf* icon_pixbuf = gfx::GdkPixbufFromSkBitmap(&icon);
559 gtk_window_set_icon(window_, icon_pixbuf);
560 g_object_unref(icon_pixbuf);
561 }
562 }
563 }
564
UpdateThrobber(TabContents * tab_contents)565 void BrowserTitlebar::UpdateThrobber(TabContents* tab_contents) {
566 DCHECK(app_mode_favicon_);
567
568 if (tab_contents && tab_contents->is_loading()) {
569 GdkPixbuf* icon_pixbuf =
570 throbber_.GetNextFrame(tab_contents->waiting_for_response());
571 gtk_image_set_from_pixbuf(GTK_IMAGE(app_mode_favicon_), icon_pixbuf);
572 } else {
573 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
574
575 // Note: this isn't browser_window_->browser()->type() & Browser::TYPE_APP
576 // because we want to exclude Browser::TYPE_APP_POPUP.
577 if (browser_window_->browser()->type() == Browser::TYPE_APP ||
578 browser_window_->browser()->type() == Browser::TYPE_APP_PANEL) {
579 SkBitmap icon = browser_window_->browser()->GetCurrentPageIcon();
580 if (icon.empty()) {
581 // Fallback to the Chromium icon if the page has no icon.
582 gtk_image_set_from_pixbuf(GTK_IMAGE(app_mode_favicon_),
583 rb.GetPixbufNamed(IDR_PRODUCT_LOGO_16));
584 } else {
585 GdkPixbuf* icon_pixbuf = gfx::GdkPixbufFromSkBitmap(&icon);
586 gtk_image_set_from_pixbuf(GTK_IMAGE(app_mode_favicon_), icon_pixbuf);
587 g_object_unref(icon_pixbuf);
588 }
589 } else {
590 gtk_image_set_from_pixbuf(GTK_IMAGE(app_mode_favicon_),
591 rb.GetPixbufNamed(IDR_PRODUCT_LOGO_16));
592 }
593 throbber_.Reset();
594 }
595 }
596
UpdateTitlebarAlignment()597 void BrowserTitlebar::UpdateTitlebarAlignment() {
598 if (browser_window_->browser()->type() == Browser::TYPE_NORMAL) {
599 int top_padding = 0;
600 int side_padding = 0;
601 int vertical_offset = kNormalVerticalOffset;
602
603 if (using_custom_frame_) {
604 if (!browser_window_->IsMaximized()) {
605 top_padding = kTitlebarHeight;
606 } else if (using_custom_frame_ && browser_window_->IsMaximized()) {
607 vertical_offset = 0;
608 side_padding = kMaximizedTabstripPadding;
609 }
610 }
611
612 int right_padding = 0;
613 int left_padding = kTabStripLeftPadding;
614 if (titlebar_right_buttons_hbox_)
615 right_padding = side_padding;
616 if (titlebar_left_buttons_hbox_)
617 left_padding = side_padding;
618
619 gtk_alignment_set_padding(GTK_ALIGNMENT(titlebar_alignment_),
620 top_padding, 0,
621 left_padding, right_padding);
622 browser_window_->tabstrip()->SetVerticalOffset(vertical_offset);
623 } else {
624 if (using_custom_frame_ && !browser_window_->IsFullscreen()) {
625 gtk_alignment_set_padding(GTK_ALIGNMENT(titlebar_alignment_),
626 kAppModePaddingTop, kAppModePaddingBottom, kAppModePaddingLeft, 0);
627 gtk_widget_show(titlebar_alignment_);
628 } else {
629 gtk_widget_hide(titlebar_alignment_);
630 }
631 }
632
633 // Resize the buttons so that the clickable area extends all the way to the
634 // edge of the browser window.
635 GtkRequisition close_button_req = close_button_req_;
636 GtkRequisition minimize_button_req = minimize_button_req_;
637 GtkRequisition restore_button_req = restore_button_req_;
638 if (using_custom_frame_ && browser_window_->IsMaximized()) {
639 close_button_req.width += kButtonOuterPadding;
640 close_button_req.height += kButtonOuterPadding;
641 minimize_button_req.height += kButtonOuterPadding;
642 restore_button_req.height += kButtonOuterPadding;
643 if (top_padding_left_)
644 gtk_widget_hide(top_padding_left_);
645 if (top_padding_right_)
646 gtk_widget_hide(top_padding_right_);
647 } else {
648 if (top_padding_left_)
649 gtk_widget_show(top_padding_left_);
650 if (top_padding_right_)
651 gtk_widget_show(top_padding_right_);
652 }
653 if (close_button_.get()) {
654 gtk_widget_set_size_request(close_button_->widget(),
655 close_button_req.width,
656 close_button_req.height);
657 }
658 if (minimize_button_.get()) {
659 gtk_widget_set_size_request(minimize_button_->widget(),
660 minimize_button_req.width,
661 minimize_button_req.height);
662 }
663 if (maximize_button_.get()) {
664 gtk_widget_set_size_request(restore_button_->widget(),
665 restore_button_req.width,
666 restore_button_req.height);
667 }
668 }
669
UpdateTextColor()670 void BrowserTitlebar::UpdateTextColor() {
671 if (!app_mode_title_)
672 return;
673
674 if (theme_service_ && theme_service_->UseGtkTheme()) {
675 // We don't really have any good options here.
676 //
677 // Colors from window manager themes aren't exposed in GTK; the window
678 // manager is a separate component and when there is information sharing
679 // (in the case of metacity), it's one way where the window manager reads
680 // data from the GTK theme (which allows us to do a decent job with
681 // picking the frame color).
682 //
683 // We probably won't match in the majority of cases, but we can at the
684 // very least make things legible. The default metacity and xfwm themes
685 // on ubuntu have white text hardcoded. Determine whether black or white
686 // has more luminosity contrast and then set that color as the text
687 // color.
688 GdkColor frame_color;
689 if (window_has_focus_) {
690 frame_color = theme_service_->GetGdkColor(
691 ThemeService::COLOR_FRAME);
692 } else {
693 frame_color = theme_service_->GetGdkColor(
694 ThemeService::COLOR_FRAME_INACTIVE);
695 }
696 GdkColor text_color = PickLuminosityContrastingColor(
697 &frame_color, >k_util::kGdkWhite, >k_util::kGdkBlack);
698 gtk_util::SetLabelColor(app_mode_title_, &text_color);
699 } else {
700 gtk_util::SetLabelColor(app_mode_title_, >k_util::kGdkWhite);
701 }
702 }
703
ShowFaviconMenu(GdkEventButton * event)704 void BrowserTitlebar::ShowFaviconMenu(GdkEventButton* event) {
705 if (!favicon_menu_model_.get()) {
706 favicon_menu_model_.reset(
707 new PopupPageMenuModel(this, browser_window_->browser()));
708
709 favicon_menu_.reset(new MenuGtk(NULL, favicon_menu_model_.get()));
710 }
711
712 favicon_menu_->PopupForWidget(app_mode_favicon_, event->button, event->time);
713 }
714
MaximizeButtonClicked()715 void BrowserTitlebar::MaximizeButtonClicked() {
716 GdkEvent* event = gtk_get_current_event();
717 if (event->button.button == 1) {
718 gtk_window_maximize(window_);
719 } else {
720 GtkWidget* widget = GTK_WIDGET(window_);
721 GdkScreen* screen = gtk_widget_get_screen(widget);
722 gint monitor = gdk_screen_get_monitor_at_window(screen, widget->window);
723 GdkRectangle screen_rect;
724 gdk_screen_get_monitor_geometry(screen, monitor, &screen_rect);
725
726 gint x, y;
727 gtk_window_get_position(window_, &x, &y);
728 gint width = widget->allocation.width;
729 gint height = widget->allocation.height;
730
731 if (event->button.button == 3) {
732 x = 0;
733 width = screen_rect.width;
734 } else if (event->button.button == 2) {
735 y = 0;
736 height = screen_rect.height;
737 }
738
739 browser_window_->SetBounds(gfx::Rect(x, y, width, height));
740 }
741 gdk_event_free(event);
742 }
743
UpdateMaximizeRestoreVisibility()744 void BrowserTitlebar::UpdateMaximizeRestoreVisibility() {
745 if (maximize_button_.get()) {
746 if (browser_window_->IsMaximized()) {
747 gtk_widget_hide(maximize_button_->widget());
748 gtk_widget_show(restore_button_->widget());
749 } else {
750 gtk_widget_hide(restore_button_->widget());
751 gtk_widget_show(maximize_button_->widget());
752 }
753 }
754 }
755
OnWindowStateChanged(GtkWindow * window,GdkEventWindowState * event)756 gboolean BrowserTitlebar::OnWindowStateChanged(GtkWindow* window,
757 GdkEventWindowState* event) {
758 UpdateMaximizeRestoreVisibility();
759 UpdateTitlebarAlignment();
760 UpdateTextColor();
761 return FALSE;
762 }
763
OnScroll(GtkWidget * widget,GdkEventScroll * event)764 gboolean BrowserTitlebar::OnScroll(GtkWidget* widget, GdkEventScroll* event) {
765 TabStripModel* tabstrip_model = browser_window_->browser()->tabstrip_model();
766 int index = tabstrip_model->active_index();
767 if (event->direction == GDK_SCROLL_LEFT ||
768 event->direction == GDK_SCROLL_UP) {
769 if (index != 0)
770 tabstrip_model->SelectPreviousTab();
771 } else if (index + 1 < tabstrip_model->count()) {
772 tabstrip_model->SelectNextTab();
773 }
774 return TRUE;
775 }
776
777 // static
OnButtonClicked(GtkWidget * button)778 void BrowserTitlebar::OnButtonClicked(GtkWidget* button) {
779 if (close_button_.get() && close_button_->widget() == button) {
780 browser_window_->Close();
781 } else if (restore_button_.get() && restore_button_->widget() == button) {
782 browser_window_->UnMaximize();
783 } else if (maximize_button_.get() && maximize_button_->widget() == button) {
784 MaximizeButtonClicked();
785 } else if (minimize_button_.get() && minimize_button_->widget() == button) {
786 gtk_window_iconify(window_);
787 }
788 }
789
OnButtonPressed(GtkWidget * widget,GdkEventButton * event)790 gboolean BrowserTitlebar::OnButtonPressed(GtkWidget* widget,
791 GdkEventButton* event) {
792 if (event->button != 1)
793 return FALSE;
794
795 ShowFaviconMenu(event);
796 return TRUE;
797 }
798
ShowContextMenu(GdkEventButton * event)799 void BrowserTitlebar::ShowContextMenu(GdkEventButton* event) {
800 if (!context_menu_.get()) {
801 context_menu_model_.reset(new ContextMenuModel(this));
802 context_menu_.reset(new MenuGtk(NULL, context_menu_model_.get()));
803 }
804
805 context_menu_->PopupAsContext(gfx::Point(event->x_root, event->y_root),
806 event->time);
807 }
808
IsCommandIdEnabled(int command_id) const809 bool BrowserTitlebar::IsCommandIdEnabled(int command_id) const {
810 if (command_id == kShowWindowDecorationsCommand)
811 return true;
812
813 return browser_window_->browser()->command_updater()->
814 IsCommandEnabled(command_id);
815 }
816
IsCommandIdChecked(int command_id) const817 bool BrowserTitlebar::IsCommandIdChecked(int command_id) const {
818 if (command_id == kShowWindowDecorationsCommand) {
819 PrefService* prefs = browser_window_->browser()->profile()->GetPrefs();
820 return !prefs->GetBoolean(prefs::kUseCustomChromeFrame);
821 }
822
823 EncodingMenuController controller;
824 if (controller.DoesCommandBelongToEncodingMenu(command_id)) {
825 TabContents* tab_contents =
826 browser_window_->browser()->GetSelectedTabContents();
827 if (tab_contents) {
828 return controller.IsItemChecked(browser_window_->browser()->profile(),
829 tab_contents->encoding(),
830 command_id);
831 }
832 return false;
833 }
834
835 NOTREACHED();
836 return false;
837 }
838
ExecuteCommand(int command_id)839 void BrowserTitlebar::ExecuteCommand(int command_id) {
840 if (command_id == kShowWindowDecorationsCommand) {
841 PrefService* prefs = browser_window_->browser()->profile()->GetPrefs();
842 prefs->SetBoolean(prefs::kUseCustomChromeFrame,
843 !prefs->GetBoolean(prefs::kUseCustomChromeFrame));
844 return;
845 }
846
847 browser_window_->browser()->ExecuteCommand(command_id);
848 }
849
GetAcceleratorForCommandId(int command_id,ui::Accelerator * accelerator)850 bool BrowserTitlebar::GetAcceleratorForCommandId(
851 int command_id, ui::Accelerator* accelerator) {
852 const ui::AcceleratorGtk* accelerator_gtk =
853 AcceleratorsGtk::GetInstance()->GetPrimaryAcceleratorForCommand(
854 command_id);
855 if (accelerator_gtk)
856 *accelerator = *accelerator_gtk;
857 return accelerator_gtk;
858 }
859
Observe(NotificationType type,const NotificationSource & source,const NotificationDetails & details)860 void BrowserTitlebar::Observe(NotificationType type,
861 const NotificationSource& source,
862 const NotificationDetails& details) {
863 switch (type.value) {
864 case NotificationType::BROWSER_THEME_CHANGED:
865 UpdateTextColor();
866 break;
867
868 default:
869 NOTREACHED();
870 }
871 }
872
ActiveWindowChanged(GdkWindow * active_window)873 void BrowserTitlebar::ActiveWindowChanged(GdkWindow* active_window) {
874 // Can be called during shutdown; BrowserWindowGtk will set our |window_|
875 // to NULL during that time.
876 if (!window_)
877 return;
878
879 window_has_focus_ = GTK_WIDGET(window_)->window == active_window;
880 UpdateTextColor();
881 }
882
883 ///////////////////////////////////////////////////////////////////////////////
884 // BrowserTitlebar::Throbber implementation
885 // TODO(tc): Handle anti-clockwise spinning when waiting for a connection.
886
887 // We don't bother to clean up these or the pixbufs they contain when we exit.
888 static std::vector<GdkPixbuf*>* g_throbber_frames = NULL;
889 static std::vector<GdkPixbuf*>* g_throbber_waiting_frames = NULL;
890
891 // Load |resource_id| from the ResourceBundle and split it into a series of
892 // square GdkPixbufs that get stored in |frames|.
MakeThrobberFrames(int resource_id,std::vector<GdkPixbuf * > * frames)893 static void MakeThrobberFrames(int resource_id,
894 std::vector<GdkPixbuf*>* frames) {
895 ResourceBundle &rb = ResourceBundle::GetSharedInstance();
896 SkBitmap* frame_strip = rb.GetBitmapNamed(resource_id);
897
898 // Each frame of the animation is a square, so we use the height as the
899 // frame size.
900 int frame_size = frame_strip->height();
901 size_t num_frames = frame_strip->width() / frame_size;
902
903 // Make a separate GdkPixbuf for each frame of the animation.
904 for (size_t i = 0; i < num_frames; ++i) {
905 SkBitmap frame = SkBitmapOperations::CreateTiledBitmap(*frame_strip,
906 i * frame_size, 0, frame_size, frame_size);
907 frames->push_back(gfx::GdkPixbufFromSkBitmap(&frame));
908 }
909 }
910
GetNextFrame(bool is_waiting)911 GdkPixbuf* BrowserTitlebar::Throbber::GetNextFrame(bool is_waiting) {
912 Throbber::InitFrames();
913 if (is_waiting) {
914 return (*g_throbber_waiting_frames)[current_waiting_frame_++ %
915 g_throbber_waiting_frames->size()];
916 } else {
917 return (*g_throbber_frames)[current_frame_++ % g_throbber_frames->size()];
918 }
919 }
920
Reset()921 void BrowserTitlebar::Throbber::Reset() {
922 current_frame_ = 0;
923 current_waiting_frame_ = 0;
924 }
925
926 // static
InitFrames()927 void BrowserTitlebar::Throbber::InitFrames() {
928 if (g_throbber_frames)
929 return;
930
931 // We load the light version of the throbber since it'll be in the titlebar.
932 g_throbber_frames = new std::vector<GdkPixbuf*>;
933 MakeThrobberFrames(IDR_THROBBER_LIGHT, g_throbber_frames);
934
935 g_throbber_waiting_frames = new std::vector<GdkPixbuf*>;
936 MakeThrobberFrames(IDR_THROBBER_WAITING_LIGHT, g_throbber_waiting_frames);
937 }
938
ContextMenuModel(ui::SimpleMenuModel::Delegate * delegate)939 BrowserTitlebar::ContextMenuModel::ContextMenuModel(
940 ui::SimpleMenuModel::Delegate* delegate)
941 : SimpleMenuModel(delegate) {
942 AddItemWithStringId(IDC_NEW_TAB, IDS_TAB_CXMENU_NEWTAB);
943 AddItemWithStringId(IDC_RESTORE_TAB, IDS_RESTORE_TAB);
944 AddSeparator();
945 AddItemWithStringId(IDC_TASK_MANAGER, IDS_TASK_MANAGER);
946 AddSeparator();
947 AddCheckItemWithStringId(kShowWindowDecorationsCommand,
948 IDS_SHOW_WINDOW_DECORATIONS_MENU);
949 }
950