blob: 84a1dab455c18f8a4c9842a2a9829a1daf0a40c1 [file] [log] [blame]
// Copyright 2016 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/ui/views/intent_picker_bubble_view.h"
#include "base/bind.h"
#include "base/i18n/rtl.h"
#include "base/logging.h"
#include "base/strings/string_piece.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/navigation_handle.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/canvas.h"
#include "ui/views/animation/ink_drop_host_view.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/scrollbar/overlay_scroll_bar.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/grid_layout.h"
#include "ui/views/window/dialog_client_view.h"
namespace {
// Using |kMaxAppResults| as a measure of how many apps we want to show.
constexpr size_t kMaxAppResults = arc::ArcNavigationThrottle::kMaxAppResults;
// Main components sizes
constexpr int kDialogDelegateInsets = 16;
constexpr int kRowHeight = 40;
constexpr int kMaxWidth = 320;
// UI position wrt the Top Container
constexpr int kTopContainerMerge = 3;
constexpr char kInvalidPackageName[] = "";
bool IsKeyboardCodeArrow(ui::KeyboardCode key_code) {
return key_code == ui::VKEY_UP || key_code == ui::VKEY_DOWN ||
key_code == ui::VKEY_RIGHT || key_code == ui::VKEY_LEFT;
}
} // namespace
// IntentPickerLabelButton
// A button that represents a candidate intent handler.
class IntentPickerLabelButton : public views::LabelButton {
public:
IntentPickerLabelButton(views::ButtonListener* listener,
gfx::Image* icon,
const std::string& package_name,
const std::string& activity_name)
: LabelButton(listener,
base::UTF8ToUTF16(base::StringPiece(activity_name))),
package_name_(package_name) {
SetHorizontalAlignment(gfx::ALIGN_LEFT);
SetMinSize(gfx::Size(kMaxWidth, kRowHeight));
SetInkDropMode(InkDropMode::ON);
if (!icon->IsEmpty())
SetImage(views::ImageButton::STATE_NORMAL, *icon->ToImageSkia());
SetBorder(views::CreateEmptyBorder(10, 16, 10, 0));
}
SkColor GetInkDropBaseColor() const override { return SK_ColorBLACK; }
void MarkAsUnselected(const ui::Event* event) {
AnimateInkDrop(views::InkDropState::HIDDEN,
ui::LocatedEvent::FromIfValid(event));
}
void MarkAsSelected(const ui::Event* event) {
AnimateInkDrop(views::InkDropState::ACTIVATED,
ui::LocatedEvent::FromIfValid(event));
}
views::InkDropState GetTargetInkDropState() {
return GetInkDrop()->GetTargetInkDropState();
}
private:
std::string package_name_;
DISALLOW_COPY_AND_ASSIGN(IntentPickerLabelButton);
};
// static
void IntentPickerBubbleView::ShowBubble(
content::WebContents* web_contents,
const std::vector<AppInfo>& app_info,
const IntentPickerResponse& intent_picker_cb) {
Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
if (!browser || !BrowserView::GetBrowserViewForBrowser(browser)) {
intent_picker_cb.Run(kInvalidPackageName,
arc::ArcNavigationThrottle::CloseReason::ERROR);
return;
}
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser);
IntentPickerBubbleView* delegate =
new IntentPickerBubbleView(app_info, intent_picker_cb, web_contents);
delegate->set_margins(gfx::Insets(16, 0, 0, 0));
delegate->set_parent_window(browser_view->GetNativeWindow());
views::Widget* widget =
views::BubbleDialogDelegateView::CreateBubble(delegate);
delegate->SetArrowPaintType(views::BubbleBorder::PAINT_NONE);
delegate->SetAlignment(views::BubbleBorder::ALIGN_EDGE_TO_ANCHOR_EDGE);
// Using the TopContainerBoundsInScreen Rect to specify an anchor for the the
// UI. Rect allow us to set the coordinates(x,y), the width and height for the
// new Rectangle.
delegate->SetAnchorRect(
gfx::Rect(browser_view->GetTopContainerBoundsInScreen().x(),
browser_view->GetTopContainerBoundsInScreen().y(),
browser_view->GetTopContainerBoundsInScreen().width(),
browser_view->GetTopContainerBoundsInScreen().height() -
kTopContainerMerge));
delegate->GetDialogClientView()->set_button_row_insets(
gfx::Insets(kDialogDelegateInsets));
delegate->GetDialogClientView()->Layout();
delegate->SetFocusBehavior(View::FocusBehavior::ALWAYS);
delegate->GetIntentPickerLabelButtonAt(0)->MarkAsSelected(nullptr);
widget->Show();
}
// static
std::unique_ptr<IntentPickerBubbleView>
IntentPickerBubbleView::CreateBubbleView(
const std::vector<AppInfo>& app_info,
const IntentPickerResponse& intent_picker_cb,
content::WebContents* web_contents) {
std::unique_ptr<IntentPickerBubbleView> bubble(
new IntentPickerBubbleView(app_info, intent_picker_cb, web_contents));
bubble->Init();
return bubble;
}
bool IntentPickerBubbleView::Accept() {
RunCallback(app_info_[selected_app_tag_].package_name,
arc::ArcNavigationThrottle::CloseReason::JUST_ONCE_PRESSED);
return true;
}
bool IntentPickerBubbleView::Cancel() {
RunCallback(app_info_[selected_app_tag_].package_name,
arc::ArcNavigationThrottle::CloseReason::ALWAYS_PRESSED);
return true;
}
bool IntentPickerBubbleView::Close() {
// Whenever closing the bubble without pressing |Just once| or |Always| we
// need to report back that the user didn't select anything.
RunCallback(kInvalidPackageName,
arc::ArcNavigationThrottle::CloseReason::DIALOG_DEACTIVATED);
return true;
}
void IntentPickerBubbleView::Init() {
views::BoxLayout* general_layout =
new views::BoxLayout(views::BoxLayout::kVertical, 0, 0, 0);
SetLayoutManager(general_layout);
// Creates a view to hold the views for each app.
views::View* scrollable_view = new views::View();
views::BoxLayout* scrollable_layout =
new views::BoxLayout(views::BoxLayout::kVertical, 0, 0, 0);
scrollable_view->SetLayoutManager(scrollable_layout);
for (size_t i = 0; i < app_info_.size(); ++i) {
IntentPickerLabelButton* app_button = new IntentPickerLabelButton(
this, &app_info_[i].icon, app_info_[i].package_name,
app_info_[i].activity_name);
app_button->set_tag(i);
scrollable_view->AddChildViewAt(app_button, i);
}
scroll_view_ = new views::ScrollView();
scroll_view_->EnableViewPortLayer();
scroll_view_->SetContents(scrollable_view);
// Setting a customized ScrollBar which is shown only when the mouse pointer
// is inside the ScrollView.
scroll_view_->SetVerticalScrollBar(new views::OverlayScrollBar(false));
// This part gives the scroll a fixed width and height. The height depends on
// how many app candidates we got and how many we actually want to show.
// The added 0.5 on the else block allow us to let the user know there are
// more than |kMaxAppResults| apps accessible by scrolling the list.
if (app_info_.size() <= kMaxAppResults) {
scroll_view_->ClipHeightTo(kRowHeight, app_info_.size() * kRowHeight);
} else {
scroll_view_->ClipHeightTo(kRowHeight, (kMaxAppResults + 0.5) * kRowHeight);
}
AddChildView(scroll_view_);
}
base::string16 IntentPickerBubbleView::GetWindowTitle() const {
return l10n_util::GetStringUTF16(IDS_INTENT_PICKER_BUBBLE_VIEW_OPEN_WITH);
}
base::string16 IntentPickerBubbleView::GetDialogButtonLabel(
ui::DialogButton button) const {
return l10n_util::GetStringUTF16(button == ui::DIALOG_BUTTON_OK
? IDS_INTENT_PICKER_BUBBLE_VIEW_JUST_ONCE
: IDS_INTENT_PICKER_BUBBLE_VIEW_ALWAYS);
}
IntentPickerBubbleView::IntentPickerBubbleView(
const std::vector<AppInfo>& app_info,
IntentPickerResponse intent_picker_cb,
content::WebContents* web_contents)
: views::BubbleDialogDelegateView(nullptr /* anchor_view */,
views::BubbleBorder::TOP_CENTER),
WebContentsObserver(web_contents),
intent_picker_cb_(intent_picker_cb),
selected_app_tag_(0),
scroll_view_(nullptr),
app_info_(app_info) {}
IntentPickerBubbleView::~IntentPickerBubbleView() {
SetLayoutManager(nullptr);
}
// If the widget gets closed without an app being selected we still need to use
// the callback so the caller can Resume the navigation.
void IntentPickerBubbleView::OnWidgetDestroying(views::Widget* widget) {
RunCallback(kInvalidPackageName,
arc::ArcNavigationThrottle::CloseReason::DIALOG_DEACTIVATED);
}
void IntentPickerBubbleView::ButtonPressed(views::Button* sender,
const ui::Event& event) {
SetSelectedAppIndex(sender->tag(), &event);
RequestFocus();
}
void IntentPickerBubbleView::ArrowButtonPressed(int index) {
SetSelectedAppIndex(index, nullptr);
AdjustScrollViewVisibleRegion();
}
gfx::Size IntentPickerBubbleView::GetPreferredSize() const {
gfx::Size ps;
ps.set_width(kMaxWidth);
int apps_height = app_info_.size();
// We are showing |kMaxAppResults| + 0.5 rows at max, the extra 0.5 is used so
// the user can notice that more options are available.
if (app_info_.size() > kMaxAppResults) {
apps_height = (kMaxAppResults + 0.5) * kRowHeight;
} else {
apps_height *= kRowHeight;
}
ps.set_height(apps_height + kDialogDelegateInsets);
return ps;
}
// If the actual web_contents gets destroyed in the middle of the process we
// should inform the caller about this error.
void IntentPickerBubbleView::WebContentsDestroyed() {
GetWidget()->Close();
}
void IntentPickerBubbleView::OnKeyEvent(ui::KeyEvent* event) {
if (!IsKeyboardCodeArrow(event->key_code()) ||
event->type() != ui::ET_KEY_RELEASED)
return;
int delta = 0;
switch (event->key_code()) {
case ui::VKEY_UP:
delta = -1;
break;
case ui::VKEY_DOWN:
delta = 1;
break;
case ui::VKEY_LEFT:
delta = base::i18n::IsRTL() ? 1 : -1;
break;
case ui::VKEY_RIGHT:
delta = base::i18n::IsRTL() ? -1 : 1;
break;
default:
NOTREACHED();
break;
}
ArrowButtonPressed(CalculateNextAppIndex(delta));
View::OnKeyEvent(event);
}
IntentPickerLabelButton* IntentPickerBubbleView::GetIntentPickerLabelButtonAt(
size_t index) {
views::View* temp_contents = scroll_view_->contents();
return static_cast<IntentPickerLabelButton*>(temp_contents->child_at(index));
}
void IntentPickerBubbleView::RunCallback(
std::string package,
arc::ArcNavigationThrottle::CloseReason close_reason) {
if (!intent_picker_cb_.is_null()) {
// We must ensure |intent_picker_cb_| is only Run() once, this is why we
// have a temporary |callback| helper, so we can set the original callback
// to null and still report back to whoever started the UI.
auto callback = intent_picker_cb_;
intent_picker_cb_.Reset();
callback.Run(package, close_reason);
}
}
size_t IntentPickerBubbleView::GetScrollViewSize() const {
return scroll_view_->contents()->child_count();
}
void IntentPickerBubbleView::AdjustScrollViewVisibleRegion() {
const views::ScrollBar* bar = scroll_view_->vertical_scroll_bar();
if (bar) {
scroll_view_->ScrollToPosition(const_cast<views::ScrollBar*>(bar),
(selected_app_tag_ - 1) * kRowHeight);
}
}
void IntentPickerBubbleView::SetSelectedAppIndex(int index,
const ui::Event* event) {
// The selected app must be a value in the range [0, app_info_.size()-1].
DCHECK_LT(static_cast<size_t>(index), app_info_.size());
GetIntentPickerLabelButtonAt(selected_app_tag_)->MarkAsUnselected(nullptr);
selected_app_tag_ = index;
GetIntentPickerLabelButtonAt(selected_app_tag_)->MarkAsSelected(event);
}
size_t IntentPickerBubbleView::CalculateNextAppIndex(int delta) {
size_t size = GetScrollViewSize();
return static_cast<size_t>((selected_app_tag_ + size + delta) % size);
}
gfx::ImageSkia IntentPickerBubbleView::GetAppImageForTesting(size_t index) {
return GetIntentPickerLabelButtonAt(index)->GetImage(
views::Button::ButtonState::STATE_NORMAL);
}
views::InkDropState IntentPickerBubbleView::GetInkDropStateForTesting(
size_t index) {
return GetIntentPickerLabelButtonAt(index)->GetTargetInkDropState();
}
void IntentPickerBubbleView::PressButtonForTesting(size_t index,
const ui::Event& event) {
views::Button* button =
static_cast<views::Button*>(GetIntentPickerLabelButtonAt(index));
ButtonPressed(button, event);
}