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/chromeos/frame/panel_controller.h"
6
7#include <vector>
8
9#include "base/logging.h"
10#include "base/memory/scoped_ptr.h"
11#include "base/memory/singleton.h"
12#include "base/string_util.h"
13#include "base/time.h"
14#include "base/utf_string_conversions.h"
15#include "chrome/browser/chromeos/wm_ipc.h"
16#include "content/common/notification_service.h"
17#include "grit/app_resources.h"
18#include "grit/generated_resources.h"
19#include "grit/theme_resources.h"
20#include "third_party/cros/chromeos_wm_ipc_enums.h"
21#include "third_party/skia/include/effects/SkBlurMaskFilter.h"
22#include "third_party/skia/include/effects/SkGradientShader.h"
23#include "ui/base/resource/resource_bundle.h"
24#include "ui/gfx/canvas_skia.h"
25#include "views/controls/button/image_button.h"
26#include "views/controls/image_view.h"
27#include "views/controls/label.h"
28#include "views/events/event.h"
29#include "views/painter.h"
30#include "views/view.h"
31#include "views/widget/widget.h"
32#include "views/window/window.h"
33
34namespace chromeos {
35
36static int close_button_width;
37static int close_button_height;
38static SkBitmap* close_button_n;
39static SkBitmap* close_button_m;
40static SkBitmap* close_button_h;
41static SkBitmap* close_button_p;
42static gfx::Font* active_font = NULL;
43static gfx::Font* inactive_font = NULL;
44
45namespace {
46
47const int kTitleHeight = 24;
48const int kTitleIconSize = 16;
49const int kTitleWidthPad = 4;
50const int kTitleHeightPad = 4;
51const int kTitleCornerRadius = 4;
52const int kTitleCloseButtonPad = 6;
53const SkColor kTitleActiveGradientStart = SK_ColorWHITE;
54const SkColor kTitleActiveGradientEnd = 0xffe7edf1;
55const SkColor kTitleUrgentGradientStart = 0xfffea044;
56const SkColor kTitleUrgentGradientEnd = 0xfffa983a;
57const SkColor kTitleActiveColor = SK_ColorBLACK;
58const SkColor kTitleInactiveColor = SK_ColorBLACK;
59const SkColor kTitleCloseButtonColor = SK_ColorBLACK;
60// Delay before the urgency can be set after it has been cleared.
61const base::TimeDelta kSetUrgentDelay = base::TimeDelta::FromMilliseconds(500);
62
63// Used to draw the background of the panel title window.
64class TitleBackgroundPainter : public views::Painter {
65 public:
66  explicit TitleBackgroundPainter(PanelController* controller)
67      : panel_controller_(controller) { }
68
69 private:
70  virtual void Paint(int w, int h, gfx::Canvas* canvas) {
71    SkRect rect = {0, 0, w, h};
72    SkPath path;
73    SkScalar corners[] = {
74        kTitleCornerRadius, kTitleCornerRadius,
75        kTitleCornerRadius, kTitleCornerRadius,
76        0, 0,
77        0, 0
78    };
79    path.addRoundRect(rect, corners);
80    SkPaint paint;
81    paint.setStyle(SkPaint::kFill_Style);
82    paint.setFlags(SkPaint::kAntiAlias_Flag);
83    SkPoint p[2] = { {0, 0}, {0, h} };
84    SkColor colors[2] = {kTitleActiveGradientStart, kTitleActiveGradientEnd};
85    if (panel_controller_->urgent()) {
86      colors[0] = kTitleUrgentGradientStart;
87      colors[1] = kTitleUrgentGradientEnd;
88    }
89    SkShader* s = SkGradientShader::CreateLinear(
90        p, colors, NULL, 2, SkShader::kClamp_TileMode, NULL);
91    paint.setShader(s);
92    // Need to unref shader, otherwise never deleted.
93    s->unref();
94    canvas->AsCanvasSkia()->drawPath(path, paint);
95  }
96
97  PanelController* panel_controller_;
98};
99
100static bool resources_initialized;
101static void InitializeResources() {
102  if (resources_initialized) {
103    return;
104  }
105
106  resources_initialized = true;
107  ResourceBundle& rb = ResourceBundle::GetSharedInstance();
108  gfx::Font base_font = rb.GetFont(ResourceBundle::BaseFont);
109  // Title fonts are the same for active and inactive.
110  inactive_font = new gfx::Font(base_font.DeriveFont(0, gfx::Font::BOLD));
111  active_font = inactive_font;
112  close_button_n = rb.GetBitmapNamed(IDR_TAB_CLOSE);
113  close_button_m = rb.GetBitmapNamed(IDR_TAB_CLOSE_MASK);
114  close_button_h = rb.GetBitmapNamed(IDR_TAB_CLOSE_H);
115  close_button_p = rb.GetBitmapNamed(IDR_TAB_CLOSE_P);
116  close_button_width = close_button_n->width();
117  close_button_height = close_button_n->height();
118}
119
120}  // namespace
121
122PanelController::PanelController(Delegate* delegate,
123                                 GtkWindow* window)
124    :  delegate_(delegate),
125       panel_(window),
126       panel_xid_(ui::GetX11WindowFromGtkWidget(GTK_WIDGET(panel_))),
127       title_window_(NULL),
128       title_(NULL),
129       title_content_(NULL),
130       expanded_(true),
131       mouse_down_(false),
132       dragging_(false),
133       client_event_handler_id_(0),
134       focused_(false),
135       urgent_(false) {
136}
137
138void PanelController::Init(bool initial_focus,
139                           const gfx::Rect& window_bounds,
140                           XID creator_xid,
141                           WmIpcPanelUserResizeType resize_type) {
142  gfx::Rect title_bounds(0, 0, window_bounds.width(), kTitleHeight);
143
144  views::Widget::CreateParams params(views::Widget::CreateParams::TYPE_WINDOW);
145  params.transparent = true;
146  title_window_ = views::Widget::CreateWidget(params);
147  title_window_->Init(NULL, title_bounds);
148  gtk_widget_set_size_request(title_window_->GetNativeView(),
149                              title_bounds.width(), title_bounds.height());
150  title_ = title_window_->GetNativeView();
151  title_xid_ = ui::GetX11WindowFromGtkWidget(title_);
152
153  WmIpc::instance()->SetWindowType(
154      title_,
155      WM_IPC_WINDOW_CHROME_PANEL_TITLEBAR,
156      NULL);
157  std::vector<int> type_params;
158  type_params.push_back(title_xid_);
159  type_params.push_back(expanded_ ? 1 : 0);
160  type_params.push_back(initial_focus ? 1 : 0);
161  type_params.push_back(creator_xid);
162  type_params.push_back(resize_type);
163  WmIpc::instance()->SetWindowType(
164      GTK_WIDGET(panel_),
165      WM_IPC_WINDOW_CHROME_PANEL_CONTENT,
166      &type_params);
167
168  client_event_handler_id_ = g_signal_connect(
169      panel_, "client-event", G_CALLBACK(OnPanelClientEvent), this);
170
171  title_content_ = new TitleContentView(this);
172  title_window_->SetContentsView(title_content_);
173  UpdateTitleBar();
174  title_window_->Show();
175}
176
177void PanelController::UpdateTitleBar() {
178  if (!delegate_ || !title_window_)
179    return;
180  title_content_->title_label()->SetText(
181      UTF16ToWideHack(delegate_->GetPanelTitle()));
182  title_content_->title_icon()->SetImage(delegate_->GetPanelIcon());
183}
184
185void PanelController::SetUrgent(bool urgent) {
186  if (!urgent)
187    urgent_cleared_time_ = base::TimeTicks::Now();
188  if (urgent == urgent_)
189    return;
190  if (urgent && focused_)
191    return;  // Don't set urgency for focused panels.
192  if (urgent && base::TimeTicks::Now() < urgent_cleared_time_ + kSetUrgentDelay)
193    return;  // Don't set urgency immediately after clearing it.
194  urgent_ = urgent;
195  if (title_window_) {
196    gtk_window_set_urgency_hint(panel_, urgent ? TRUE : FALSE);
197    title_content_->SchedulePaint();
198  }
199}
200
201bool PanelController::TitleMousePressed(const views::MouseEvent& event) {
202  if (!event.IsOnlyLeftMouseButton())
203    return false;
204  GdkEvent* gdk_event = gtk_get_current_event();
205  if (gdk_event->type != GDK_BUTTON_PRESS) {
206    gdk_event_free(gdk_event);
207    NOTREACHED();
208    return false;
209  }
210  DCHECK(title_);
211  // Get the last titlebar width that we saw in a ConfigureNotify event -- we
212  // need to give drag positions in terms of the top-right corner of the
213  // titlebar window.  See WM_IPC_MESSAGE_WM_NOTIFY_PANEL_DRAGGED's declaration
214  // for details.
215  gint title_width = 1;
216  gtk_window_get_size(GTK_WINDOW(title_), &title_width, NULL);
217
218  GdkEventButton last_button_event = gdk_event->button;
219  mouse_down_ = true;
220  mouse_down_abs_x_ = last_button_event.x_root;
221  mouse_down_abs_y_ = last_button_event.y_root;
222  mouse_down_offset_x_ = event.x() - title_width;
223  mouse_down_offset_y_ = event.y();
224  dragging_ = false;
225  gdk_event_free(gdk_event);
226  return true;
227}
228
229void PanelController::TitleMouseReleased(const views::MouseEvent& event) {
230  if (event.IsLeftMouseButton())
231    TitleMouseCaptureLost();
232}
233
234void PanelController::TitleMouseCaptureLost() {
235  // Only handle clicks that started in our window.
236  if (!mouse_down_)
237    return;
238
239  mouse_down_ = false;
240  if (!dragging_) {
241    if (expanded_) {
242      // Always activate the panel here, even if we are about to minimize it.
243      // This lets panels like GTalk know that they have been acknowledged, so
244      // they don't change the title again (which would trigger SetUrgent).
245      // Activating the panel also clears the urgent state.
246      delegate_->ActivatePanel();
247      SetState(PanelController::MINIMIZED);
248    } else {
249      // If we're expanding the panel, do so before focusing it.  This lets the
250      // window manager know that the panel is being expanded in response to a
251      // user action; see http://crosbug.com/14735.
252      SetState(PanelController::EXPANDED);
253      delegate_->ActivatePanel();
254    }
255  } else {
256    WmIpc::Message msg(WM_IPC_MESSAGE_WM_NOTIFY_PANEL_DRAG_COMPLETE);
257    msg.set_param(0, panel_xid_);
258    WmIpc::instance()->SendMessage(msg);
259    dragging_ = false;
260  }
261}
262
263void PanelController::SetState(State state) {
264  WmIpc::Message msg(WM_IPC_MESSAGE_WM_SET_PANEL_STATE);
265  msg.set_param(0, panel_xid_);
266  msg.set_param(1, state == EXPANDED);
267  WmIpc::instance()->SendMessage(msg);
268}
269
270bool PanelController::TitleMouseDragged(const views::MouseEvent& event) {
271  if (!mouse_down_)
272    return false;
273  GdkEvent* gdk_event = gtk_get_current_event();
274  if (gdk_event->type != GDK_MOTION_NOTIFY) {
275    gdk_event_free(gdk_event);
276    NOTREACHED();
277    return false;
278  }
279  GdkEventMotion last_motion_event = gdk_event->motion;
280  if (!dragging_) {
281    if (views::View::ExceededDragThreshold(
282        last_motion_event.x_root - mouse_down_abs_x_,
283        last_motion_event.y_root - mouse_down_abs_y_)) {
284      dragging_ = true;
285    }
286  }
287  if (dragging_) {
288    WmIpc::Message msg(WM_IPC_MESSAGE_WM_NOTIFY_PANEL_DRAGGED);
289    msg.set_param(0, panel_xid_);
290    msg.set_param(1, last_motion_event.x_root - mouse_down_offset_x_);
291    msg.set_param(2, last_motion_event.y_root - mouse_down_offset_y_);
292    WmIpc::instance()->SendMessage(msg);
293  }
294  gdk_event_free(gdk_event);
295  return true;
296}
297
298// static
299bool PanelController::OnPanelClientEvent(
300    GtkWidget* widget,
301    GdkEventClient* event,
302    PanelController* panel_controller) {
303  return panel_controller->PanelClientEvent(event);
304}
305
306void PanelController::OnFocusIn() {
307  if (title_window_)
308    title_content_->OnFocusIn();
309  focused_ = true;
310  // Clear urgent when focused.
311  SetUrgent(false);
312}
313
314void PanelController::OnFocusOut() {
315  focused_ = false;
316  if (title_window_)
317    title_content_->OnFocusOut();
318}
319
320bool PanelController::PanelClientEvent(GdkEventClient* event) {
321  WmIpc::Message msg;
322  WmIpc::instance()->DecodeMessage(*event, &msg);
323  if (msg.type() == WM_IPC_MESSAGE_CHROME_NOTIFY_PANEL_STATE) {
324    bool new_state = msg.param(0);
325    if (expanded_ != new_state) {
326      expanded_ = new_state;
327      State state = new_state ? EXPANDED : MINIMIZED;
328      NotificationService::current()->Notify(
329          NotificationType::PANEL_STATE_CHANGED,
330          Source<PanelController>(this),
331          Details<State>(&state));
332    }
333  }
334  return true;
335}
336
337void PanelController::Close() {
338  if (client_event_handler_id_ > 0) {
339    g_signal_handler_disconnect(panel_, client_event_handler_id_);
340    client_event_handler_id_ = 0;
341  }
342  // ignore if the title window is already closed.
343  if (title_window_) {
344    title_window_->Close();
345    title_window_ = NULL;
346    title_ = NULL;
347    title_content_->OnClose();
348    title_content_ = NULL;
349  }
350}
351
352void PanelController::OnCloseButtonPressed() {
353  DCHECK(title_content_);
354  if (title_window_) {
355    if (delegate_) {
356      if (!delegate_->CanClosePanel())
357        return;
358      delegate_->ClosePanel();
359    }
360    Close();
361  }
362}
363
364PanelController::TitleContentView::TitleContentView(
365    PanelController* panel_controller)
366        : panel_controller_(panel_controller) {
367  VLOG(1) << "panel: c " << this;
368  InitializeResources();
369  close_button_ = new views::ImageButton(this);
370  close_button_->SetImage(views::CustomButton::BS_NORMAL, close_button_n);
371  close_button_->SetImage(views::CustomButton::BS_HOT, close_button_h);
372  close_button_->SetImage(views::CustomButton::BS_PUSHED, close_button_p);
373  close_button_->SetBackground(
374      kTitleCloseButtonColor, close_button_n, close_button_m);
375  AddChildView(close_button_);
376
377  title_icon_ = new views::ImageView();
378  AddChildView(title_icon_);
379  title_label_ = new views::Label(std::wstring());
380  title_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
381  AddChildView(title_label_);
382
383  set_background(
384      views::Background::CreateBackgroundPainter(
385          true, new TitleBackgroundPainter(panel_controller)));
386  OnFocusOut();
387}
388
389void PanelController::TitleContentView::Layout() {
390  int close_button_x = bounds().width() -
391      (close_button_width + kTitleCloseButtonPad);
392  close_button_->SetBounds(
393      close_button_x,
394      (bounds().height() - close_button_height) / 2,
395      close_button_width,
396      close_button_height);
397  title_icon_->SetBounds(
398      kTitleWidthPad,
399      kTitleHeightPad,
400      kTitleIconSize,
401      kTitleIconSize);
402  int title_x = kTitleWidthPad * 2 + kTitleIconSize;
403  title_label_->SetBounds(
404      title_x,
405      0,
406      close_button_x - (title_x + kTitleCloseButtonPad),
407      bounds().height());
408}
409
410bool PanelController::TitleContentView::OnMousePressed(
411    const views::MouseEvent& event) {
412  return panel_controller_->TitleMousePressed(event);
413}
414
415void PanelController::TitleContentView::OnMouseReleased(
416    const views::MouseEvent& event) {
417  panel_controller_->TitleMouseReleased(event);
418}
419
420void PanelController::TitleContentView::OnMouseCaptureLost() {
421  panel_controller_->TitleMouseCaptureLost();
422}
423
424bool PanelController::TitleContentView::OnMouseDragged(
425    const views::MouseEvent& event) {
426  return panel_controller_->TitleMouseDragged(event);
427}
428
429void PanelController::TitleContentView::OnFocusIn() {
430  title_label_->SetColor(kTitleActiveColor);
431  title_label_->SetFont(*active_font);
432  Layout();
433  SchedulePaint();
434}
435
436void PanelController::TitleContentView::OnFocusOut() {
437  title_label_->SetColor(kTitleInactiveColor);
438  title_label_->SetFont(*inactive_font);
439  Layout();
440  SchedulePaint();
441}
442
443void PanelController::TitleContentView::OnClose() {
444  panel_controller_ = NULL;
445}
446
447void PanelController::TitleContentView::ButtonPressed(
448    views::Button* sender, const views::Event& event) {
449  if (panel_controller_ && sender == close_button_)
450    panel_controller_->OnCloseButtonPressed();
451}
452
453PanelController::TitleContentView::~TitleContentView() {
454  VLOG(1) << "panel: delete " << this;
455}
456
457}  // namespace chromeos
458