// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/frame/panel_controller.h"
#include <vector>
#include "base/logging.h"
#include "base/memory/scoped_ptr.h"
#include "base/memory/singleton.h"
#include "base/string_util.h"
#include "base/time.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/chromeos/wm_ipc.h"
#include "content/common/notification_service.h"
#include "grit/app_resources.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "third_party/cros/chromeos_wm_ipc_enums.h"
#include "third_party/skia/include/effects/SkBlurMaskFilter.h"
#include "third_party/skia/include/effects/SkGradientShader.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/canvas_skia.h"
#include "views/controls/button/image_button.h"
#include "views/controls/image_view.h"
#include "views/controls/label.h"
#include "views/events/event.h"
#include "views/painter.h"
#include "views/view.h"
#include "views/widget/widget.h"
#include "views/window/window.h"
namespace chromeos {
static int close_button_width;
static int close_button_height;
static SkBitmap* close_button_n;
static SkBitmap* close_button_m;
static SkBitmap* close_button_h;
static SkBitmap* close_button_p;
static gfx::Font* active_font = NULL;
static gfx::Font* inactive_font = NULL;
namespace {
const int kTitleHeight = 24;
const int kTitleIconSize = 16;
const int kTitleWidthPad = 4;
const int kTitleHeightPad = 4;
const int kTitleCornerRadius = 4;
const int kTitleCloseButtonPad = 6;
const SkColor kTitleActiveGradientStart = SK_ColorWHITE;
const SkColor kTitleActiveGradientEnd = 0xffe7edf1;
const SkColor kTitleUrgentGradientStart = 0xfffea044;
const SkColor kTitleUrgentGradientEnd = 0xfffa983a;
const SkColor kTitleActiveColor = SK_ColorBLACK;
const SkColor kTitleInactiveColor = SK_ColorBLACK;
const SkColor kTitleCloseButtonColor = SK_ColorBLACK;
// Delay before the urgency can be set after it has been cleared.
const base::TimeDelta kSetUrgentDelay = base::TimeDelta::FromMilliseconds(500);
// Used to draw the background of the panel title window.
class TitleBackgroundPainter : public views::Painter {
public:
explicit TitleBackgroundPainter(PanelController* controller)
: panel_controller_(controller) { }
private:
virtual void Paint(int w, int h, gfx::Canvas* canvas) {
SkRect rect = {0, 0, w, h};
SkPath path;
SkScalar corners[] = {
kTitleCornerRadius, kTitleCornerRadius,
kTitleCornerRadius, kTitleCornerRadius,
0, 0,
0, 0
};
path.addRoundRect(rect, corners);
SkPaint paint;
paint.setStyle(SkPaint::kFill_Style);
paint.setFlags(SkPaint::kAntiAlias_Flag);
SkPoint p[2] = { {0, 0}, {0, h} };
SkColor colors[2] = {kTitleActiveGradientStart, kTitleActiveGradientEnd};
if (panel_controller_->urgent()) {
colors[0] = kTitleUrgentGradientStart;
colors[1] = kTitleUrgentGradientEnd;
}
SkShader* s = SkGradientShader::CreateLinear(
p, colors, NULL, 2, SkShader::kClamp_TileMode, NULL);
paint.setShader(s);
// Need to unref shader, otherwise never deleted.
s->unref();
canvas->AsCanvasSkia()->drawPath(path, paint);
}
PanelController* panel_controller_;
};
static bool resources_initialized;
static void InitializeResources() {
if (resources_initialized) {
return;
}
resources_initialized = true;
ResourceBundle& rb = ResourceBundle::GetSharedInstance();
gfx::Font base_font = rb.GetFont(ResourceBundle::BaseFont);
// Title fonts are the same for active and inactive.
inactive_font = new gfx::Font(base_font.DeriveFont(0, gfx::Font::BOLD));
active_font = inactive_font;
close_button_n = rb.GetBitmapNamed(IDR_TAB_CLOSE);
close_button_m = rb.GetBitmapNamed(IDR_TAB_CLOSE_MASK);
close_button_h = rb.GetBitmapNamed(IDR_TAB_CLOSE_H);
close_button_p = rb.GetBitmapNamed(IDR_TAB_CLOSE_P);
close_button_width = close_button_n->width();
close_button_height = close_button_n->height();
}
} // namespace
PanelController::PanelController(Delegate* delegate,
GtkWindow* window)
: delegate_(delegate),
panel_(window),
panel_xid_(ui::GetX11WindowFromGtkWidget(GTK_WIDGET(panel_))),
title_window_(NULL),
title_(NULL),
title_content_(NULL),
expanded_(true),
mouse_down_(false),
dragging_(false),
client_event_handler_id_(0),
focused_(false),
urgent_(false) {
}
void PanelController::Init(bool initial_focus,
const gfx::Rect& window_bounds,
XID creator_xid,
WmIpcPanelUserResizeType resize_type) {
gfx::Rect title_bounds(0, 0, window_bounds.width(), kTitleHeight);
views::Widget::CreateParams params(views::Widget::CreateParams::TYPE_WINDOW);
params.transparent = true;
title_window_ = views::Widget::CreateWidget(params);
title_window_->Init(NULL, title_bounds);
gtk_widget_set_size_request(title_window_->GetNativeView(),
title_bounds.width(), title_bounds.height());
title_ = title_window_->GetNativeView();
title_xid_ = ui::GetX11WindowFromGtkWidget(title_);
WmIpc::instance()->SetWindowType(
title_,
WM_IPC_WINDOW_CHROME_PANEL_TITLEBAR,
NULL);
std::vector<int> type_params;
type_params.push_back(title_xid_);
type_params.push_back(expanded_ ? 1 : 0);
type_params.push_back(initial_focus ? 1 : 0);
type_params.push_back(creator_xid);
type_params.push_back(resize_type);
WmIpc::instance()->SetWindowType(
GTK_WIDGET(panel_),
WM_IPC_WINDOW_CHROME_PANEL_CONTENT,
&type_params);
client_event_handler_id_ = g_signal_connect(
panel_, "client-event", G_CALLBACK(OnPanelClientEvent), this);
title_content_ = new TitleContentView(this);
title_window_->SetContentsView(title_content_);
UpdateTitleBar();
title_window_->Show();
}
void PanelController::UpdateTitleBar() {
if (!delegate_ || !title_window_)
return;
title_content_->title_label()->SetText(
UTF16ToWideHack(delegate_->GetPanelTitle()));
title_content_->title_icon()->SetImage(delegate_->GetPanelIcon());
}
void PanelController::SetUrgent(bool urgent) {
if (!urgent)
urgent_cleared_time_ = base::TimeTicks::Now();
if (urgent == urgent_)
return;
if (urgent && focused_)
return; // Don't set urgency for focused panels.
if (urgent && base::TimeTicks::Now() < urgent_cleared_time_ + kSetUrgentDelay)
return; // Don't set urgency immediately after clearing it.
urgent_ = urgent;
if (title_window_) {
gtk_window_set_urgency_hint(panel_, urgent ? TRUE : FALSE);
title_content_->SchedulePaint();
}
}
bool PanelController::TitleMousePressed(const views::MouseEvent& event) {
if (!event.IsOnlyLeftMouseButton())
return false;
GdkEvent* gdk_event = gtk_get_current_event();
if (gdk_event->type != GDK_BUTTON_PRESS) {
gdk_event_free(gdk_event);
NOTREACHED();
return false;
}
DCHECK(title_);
// Get the last titlebar width that we saw in a ConfigureNotify event -- we
// need to give drag positions in terms of the top-right corner of the
// titlebar window. See WM_IPC_MESSAGE_WM_NOTIFY_PANEL_DRAGGED's declaration
// for details.
gint title_width = 1;
gtk_window_get_size(GTK_WINDOW(title_), &title_width, NULL);
GdkEventButton last_button_event = gdk_event->button;
mouse_down_ = true;
mouse_down_abs_x_ = last_button_event.x_root;
mouse_down_abs_y_ = last_button_event.y_root;
mouse_down_offset_x_ = event.x() - title_width;
mouse_down_offset_y_ = event.y();
dragging_ = false;
gdk_event_free(gdk_event);
return true;
}
void PanelController::TitleMouseReleased(const views::MouseEvent& event) {
if (event.IsLeftMouseButton())
TitleMouseCaptureLost();
}
void PanelController::TitleMouseCaptureLost() {
// Only handle clicks that started in our window.
if (!mouse_down_)
return;
mouse_down_ = false;
if (!dragging_) {
if (expanded_) {
// Always activate the panel here, even if we are about to minimize it.
// This lets panels like GTalk know that they have been acknowledged, so
// they don't change the title again (which would trigger SetUrgent).
// Activating the panel also clears the urgent state.
delegate_->ActivatePanel();
SetState(PanelController::MINIMIZED);
} else {
// If we're expanding the panel, do so before focusing it. This lets the
// window manager know that the panel is being expanded in response to a
// user action; see http://crosbug.com/14735.
SetState(PanelController::EXPANDED);
delegate_->ActivatePanel();
}
} else {
WmIpc::Message msg(WM_IPC_MESSAGE_WM_NOTIFY_PANEL_DRAG_COMPLETE);
msg.set_param(0, panel_xid_);
WmIpc::instance()->SendMessage(msg);
dragging_ = false;
}
}
void PanelController::SetState(State state) {
WmIpc::Message msg(WM_IPC_MESSAGE_WM_SET_PANEL_STATE);
msg.set_param(0, panel_xid_);
msg.set_param(1, state == EXPANDED);
WmIpc::instance()->SendMessage(msg);
}
bool PanelController::TitleMouseDragged(const views::MouseEvent& event) {
if (!mouse_down_)
return false;
GdkEvent* gdk_event = gtk_get_current_event();
if (gdk_event->type != GDK_MOTION_NOTIFY) {
gdk_event_free(gdk_event);
NOTREACHED();
return false;
}
GdkEventMotion last_motion_event = gdk_event->motion;
if (!dragging_) {
if (views::View::ExceededDragThreshold(
last_motion_event.x_root - mouse_down_abs_x_,
last_motion_event.y_root - mouse_down_abs_y_)) {
dragging_ = true;
}
}
if (dragging_) {
WmIpc::Message msg(WM_IPC_MESSAGE_WM_NOTIFY_PANEL_DRAGGED);
msg.set_param(0, panel_xid_);
msg.set_param(1, last_motion_event.x_root - mouse_down_offset_x_);
msg.set_param(2, last_motion_event.y_root - mouse_down_offset_y_);
WmIpc::instance()->SendMessage(msg);
}
gdk_event_free(gdk_event);
return true;
}
// static
bool PanelController::OnPanelClientEvent(
GtkWidget* widget,
GdkEventClient* event,
PanelController* panel_controller) {
return panel_controller->PanelClientEvent(event);
}
void PanelController::OnFocusIn() {
if (title_window_)
title_content_->OnFocusIn();
focused_ = true;
// Clear urgent when focused.
SetUrgent(false);
}
void PanelController::OnFocusOut() {
focused_ = false;
if (title_window_)
title_content_->OnFocusOut();
}
bool PanelController::PanelClientEvent(GdkEventClient* event) {
WmIpc::Message msg;
WmIpc::instance()->DecodeMessage(*event, &msg);
if (msg.type() == WM_IPC_MESSAGE_CHROME_NOTIFY_PANEL_STATE) {
bool new_state = msg.param(0);
if (expanded_ != new_state) {
expanded_ = new_state;
State state = new_state ? EXPANDED : MINIMIZED;
NotificationService::current()->Notify(
NotificationType::PANEL_STATE_CHANGED,
Source<PanelController>(this),
Details<State>(&state));
}
}
return true;
}
void PanelController::Close() {
if (client_event_handler_id_ > 0) {
g_signal_handler_disconnect(panel_, client_event_handler_id_);
client_event_handler_id_ = 0;
}
// ignore if the title window is already closed.
if (title_window_) {
title_window_->Close();
title_window_ = NULL;
title_ = NULL;
title_content_->OnClose();
title_content_ = NULL;
}
}
void PanelController::OnCloseButtonPressed() {
DCHECK(title_content_);
if (title_window_) {
if (delegate_) {
if (!delegate_->CanClosePanel())
return;
delegate_->ClosePanel();
}
Close();
}
}
PanelController::TitleContentView::TitleContentView(
PanelController* panel_controller)
: panel_controller_(panel_controller) {
VLOG(1) << "panel: c " << this;
InitializeResources();
close_button_ = new views::ImageButton(this);
close_button_->SetImage(views::CustomButton::BS_NORMAL, close_button_n);
close_button_->SetImage(views::CustomButton::BS_HOT, close_button_h);
close_button_->SetImage(views::CustomButton::BS_PUSHED, close_button_p);
close_button_->SetBackground(
kTitleCloseButtonColor, close_button_n, close_button_m);
AddChildView(close_button_);
title_icon_ = new views::ImageView();
AddChildView(title_icon_);
title_label_ = new views::Label(std::wstring());
title_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
AddChildView(title_label_);
set_background(
views::Background::CreateBackgroundPainter(
true, new TitleBackgroundPainter(panel_controller)));
OnFocusOut();
}
void PanelController::TitleContentView::Layout() {
int close_button_x = bounds().width() -
(close_button_width + kTitleCloseButtonPad);
close_button_->SetBounds(
close_button_x,
(bounds().height() - close_button_height) / 2,
close_button_width,
close_button_height);
title_icon_->SetBounds(
kTitleWidthPad,
kTitleHeightPad,
kTitleIconSize,
kTitleIconSize);
int title_x = kTitleWidthPad * 2 + kTitleIconSize;
title_label_->SetBounds(
title_x,
0,
close_button_x - (title_x + kTitleCloseButtonPad),
bounds().height());
}
bool PanelController::TitleContentView::OnMousePressed(
const views::MouseEvent& event) {
return panel_controller_->TitleMousePressed(event);
}
void PanelController::TitleContentView::OnMouseReleased(
const views::MouseEvent& event) {
panel_controller_->TitleMouseReleased(event);
}
void PanelController::TitleContentView::OnMouseCaptureLost() {
panel_controller_->TitleMouseCaptureLost();
}
bool PanelController::TitleContentView::OnMouseDragged(
const views::MouseEvent& event) {
return panel_controller_->TitleMouseDragged(event);
}
void PanelController::TitleContentView::OnFocusIn() {
title_label_->SetColor(kTitleActiveColor);
title_label_->SetFont(*active_font);
Layout();
SchedulePaint();
}
void PanelController::TitleContentView::OnFocusOut() {
title_label_->SetColor(kTitleInactiveColor);
title_label_->SetFont(*inactive_font);
Layout();
SchedulePaint();
}
void PanelController::TitleContentView::OnClose() {
panel_controller_ = NULL;
}
void PanelController::TitleContentView::ButtonPressed(
views::Button* sender, const views::Event& event) {
if (panel_controller_ && sender == close_button_)
panel_controller_->OnCloseButtonPressed();
}
PanelController::TitleContentView::~TitleContentView() {
VLOG(1) << "panel: delete " << this;
}
} // namespace chromeos