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/views/create_application_shortcut_view.h"
6
7#include "base/callback.h"
8#include "base/utf_string_conversions.h"
9#include "base/win/windows_version.h"
10#include "chrome/browser/extensions/extension_tab_helper.h"
11#include "chrome/browser/prefs/pref_service.h"
12#include "chrome/browser/profiles/profile.h"
13#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h"
14#include "chrome/browser/ui/web_applications/web_app_ui.h"
15#include "chrome/common/chrome_constants.h"
16#include "chrome/common/extensions/extension.h"
17#include "chrome/common/extensions/extension_resource.h"
18#include "chrome/common/pref_names.h"
19#include "content/browser/tab_contents/tab_contents.h"
20#include "content/browser/tab_contents/tab_contents_delegate.h"
21#include "grit/generated_resources.h"
22#include "grit/locale_settings.h"
23#include "net/base/load_flags.h"
24#include "net/url_request/url_request.h"
25#include "third_party/skia/include/core/SkRect.h"
26#include "third_party/skia/include/core/SkPaint.h"
27#include "ui/base/l10n/l10n_util.h"
28#include "ui/base/resource/resource_bundle.h"
29#include "ui/gfx/canvas_skia.h"
30#include "ui/gfx/codec/png_codec.h"
31#include "views/controls/button/checkbox.h"
32#include "views/controls/image_view.h"
33#include "views/controls/label.h"
34#include "views/layout/grid_layout.h"
35#include "views/layout/layout_constants.h"
36#include "views/window/window.h"
37
38namespace {
39
40const int kAppIconSize = 32;
41
42// AppInfoView shows the application icon and title.
43class AppInfoView : public views::View {
44 public:
45  AppInfoView(const string16& title,
46              const string16& description,
47              const SkBitmap& icon);
48
49  // Updates the title/description of the web app.
50  void UpdateText(const string16& title, const string16& description);
51
52  // Updates the icon of the web app.
53  void UpdateIcon(const SkBitmap& new_icon);
54
55  // Overridden from views::View:
56  virtual void OnPaint(gfx::Canvas* canvas);
57
58 private:
59  // Initializes the controls
60  void Init(const string16& title,
61            const string16& description, const SkBitmap& icon);
62
63  // Creates or updates description label.
64  void PrepareDescriptionLabel(const string16& description);
65
66  // Sets up layout manager.
67  void SetupLayout();
68
69  views::ImageView* icon_;
70  views::Label* title_;
71  views::Label* description_;
72};
73
74AppInfoView::AppInfoView(const string16& title,
75                         const string16& description,
76                         const SkBitmap& icon)
77    : icon_(NULL),
78      title_(NULL),
79      description_(NULL) {
80  Init(title, description, icon);
81}
82
83void AppInfoView::Init(const string16& title_text,
84                       const string16& description_text,
85                       const SkBitmap& icon) {
86  icon_ = new views::ImageView();
87  icon_->SetImage(icon);
88  icon_->SetImageSize(gfx::Size(kAppIconSize, kAppIconSize));
89
90  title_ = new views::Label(UTF16ToWide(title_text));
91  title_->SetMultiLine(true);
92  title_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
93  title_->SetFont(ResourceBundle::GetSharedInstance().GetFont(
94      ResourceBundle::BaseFont).DeriveFont(0, gfx::Font::BOLD));
95
96  if (!description_text.empty()) {
97    PrepareDescriptionLabel(description_text);
98  }
99
100  SetupLayout();
101}
102
103void AppInfoView::PrepareDescriptionLabel(const string16& description) {
104  DCHECK(!description.empty());
105
106  static const size_t kMaxLength = 200;
107  static const wchar_t* const kEllipsis = L" ... ";
108
109  std::wstring text = UTF16ToWide(description);
110  if (text.length() > kMaxLength) {
111    text = text.substr(0, kMaxLength);
112    text += kEllipsis;
113  }
114
115  if (description_) {
116    description_->SetText(text);
117  } else {
118    description_ = new views::Label(text);
119    description_->SetMultiLine(true);
120    description_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
121  }
122}
123
124void AppInfoView::SetupLayout() {
125  views::GridLayout* layout = views::GridLayout::CreatePanel(this);
126  SetLayoutManager(layout);
127
128  static const int kColumnSetId = 0;
129  views::ColumnSet* column_set = layout->AddColumnSet(kColumnSetId);
130  column_set->AddColumn(views::GridLayout::CENTER, views::GridLayout::LEADING,
131                        20.0f, views::GridLayout::FIXED,
132                        kAppIconSize, kAppIconSize);
133  column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::CENTER,
134                        80.0f, views::GridLayout::USE_PREF, 0, 0);
135
136  layout->StartRow(0, kColumnSetId);
137  layout->AddView(icon_, 1, description_ ? 2 : 1);
138  layout->AddView(title_);
139
140  if (description_) {
141    layout->StartRow(0, kColumnSetId);
142    layout->SkipColumns(1);
143    layout->AddView(description_);
144  }
145}
146
147void AppInfoView::UpdateText(const string16& title,
148                             const string16& description) {
149  title_->SetText(UTF16ToWide(title));
150  PrepareDescriptionLabel(description);
151
152  SetupLayout();
153}
154
155void AppInfoView::UpdateIcon(const SkBitmap& new_icon) {
156  DCHECK(icon_ != NULL);
157
158  icon_->SetImage(new_icon);
159}
160
161void AppInfoView::OnPaint(gfx::Canvas* canvas) {
162  gfx::Rect bounds = GetLocalBounds();
163
164  SkRect border_rect = {
165    SkIntToScalar(bounds.x()),
166    SkIntToScalar(bounds.y()),
167    SkIntToScalar(bounds.right()),
168    SkIntToScalar(bounds.bottom())
169  };
170
171  SkPaint border_paint;
172  border_paint.setAntiAlias(true);
173  border_paint.setARGB(0xFF, 0xC8, 0xC8, 0xC8);
174
175  canvas->AsCanvasSkia()->drawRoundRect(
176      border_rect, SkIntToScalar(2), SkIntToScalar(2), border_paint);
177
178  SkRect inner_rect = {
179    border_rect.fLeft + SkDoubleToScalar(0.5),
180    border_rect.fTop + SkDoubleToScalar(0.5),
181    border_rect.fRight - SkDoubleToScalar(0.5),
182    border_rect.fBottom - SkDoubleToScalar(0.5),
183  };
184
185  SkPaint inner_paint;
186  inner_paint.setAntiAlias(true);
187  inner_paint.setARGB(0xFF, 0xF8, 0xF8, 0xF8);
188  canvas->AsCanvasSkia()->drawRoundRect(
189      inner_rect, SkIntToScalar(1.5), SkIntToScalar(1.5), inner_paint);
190}
191
192}  // namespace
193
194namespace browser {
195
196void ShowCreateWebAppShortcutsDialog(gfx::NativeWindow parent_window,
197                                     TabContentsWrapper* tab_contents) {
198  views::Window::CreateChromeWindow(parent_window, gfx::Rect(),
199      new CreateUrlApplicationShortcutView(tab_contents))->Show();
200}
201
202void ShowCreateChromeAppShortcutsDialog(gfx::NativeWindow parent_window,
203                                        Profile* profile,
204                                        const Extension* app) {
205  views::Window::CreateChromeWindow(parent_window, gfx::Rect(),
206      new CreateChromeApplicationShortcutView(profile, app))->Show();
207}
208
209}  // namespace browser
210
211class CreateUrlApplicationShortcutView::IconDownloadCallbackFunctor {
212 public:
213  explicit IconDownloadCallbackFunctor(CreateUrlApplicationShortcutView* owner)
214      : owner_(owner) {
215  }
216
217  void Run(int download_id, bool errored, const SkBitmap& image) {
218    if (owner_)
219      owner_->OnIconDownloaded(errored, image);
220    delete this;
221  }
222
223  void Cancel() {
224    owner_ = NULL;
225  }
226
227 private:
228  CreateUrlApplicationShortcutView* owner_;
229};
230
231CreateApplicationShortcutView::CreateApplicationShortcutView(Profile* profile)
232    : profile_(profile) {}
233
234CreateApplicationShortcutView::~CreateApplicationShortcutView() {}
235
236void CreateApplicationShortcutView::InitControls() {
237  // Create controls
238  app_info_ = new AppInfoView(shortcut_info_.title, shortcut_info_.description,
239                              shortcut_info_.favicon);
240  create_shortcuts_label_ = new views::Label(
241      UTF16ToWide(l10n_util::GetStringUTF16(IDS_CREATE_SHORTCUTS_LABEL)));
242  create_shortcuts_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
243
244  desktop_check_box_ = AddCheckbox(UTF16ToWide(
245      l10n_util::GetStringUTF16(IDS_CREATE_SHORTCUTS_DESKTOP_CHKBOX)),
246      profile_->GetPrefs()->GetBoolean(prefs::kWebAppCreateOnDesktop));
247
248  menu_check_box_ = NULL;
249  quick_launch_check_box_ = NULL;
250
251#if defined(OS_WIN)
252  menu_check_box_ = AddCheckbox(UTF16ToWide(
253      l10n_util::GetStringUTF16(IDS_CREATE_SHORTCUTS_START_MENU_CHKBOX)),
254      profile_->GetPrefs()->GetBoolean(prefs::kWebAppCreateInAppsMenu));
255
256  quick_launch_check_box_ = AddCheckbox(
257      (base::win::GetVersion() >= base::win::VERSION_WIN7) ?
258        UTF16ToWide(l10n_util::GetStringUTF16(IDS_PIN_TO_TASKBAR_CHKBOX)) :
259        UTF16ToWide(l10n_util::GetStringUTF16(
260            IDS_CREATE_SHORTCUTS_QUICK_LAUNCH_BAR_CHKBOX)),
261      profile_->GetPrefs()->GetBoolean(prefs::kWebAppCreateInQuickLaunchBar));
262#elif defined(OS_LINUX)
263  menu_check_box_ = AddCheckbox(
264      UTF16ToWide(l10n_util::GetStringUTF16(IDS_CREATE_SHORTCUTS_MENU_CHKBOX)),
265      profile_->GetPrefs()->GetBoolean(prefs::kWebAppCreateInAppsMenu));
266#endif
267
268  // Layout controls
269  views::GridLayout* layout = views::GridLayout::CreatePanel(this);
270  SetLayoutManager(layout);
271
272  static const int kHeaderColumnSetId = 0;
273  views::ColumnSet* column_set = layout->AddColumnSet(kHeaderColumnSetId);
274  column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::CENTER,
275                        100.0f, views::GridLayout::FIXED, 0, 0);
276
277  static const int kTableColumnSetId = 1;
278  column_set = layout->AddColumnSet(kTableColumnSetId);
279  column_set->AddPaddingColumn(5.0f, 10);
280  column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
281                        100.0f, views::GridLayout::USE_PREF, 0, 0);
282
283  layout->StartRow(0, kHeaderColumnSetId);
284  layout->AddView(app_info_);
285
286  layout->AddPaddingRow(0, views::kPanelSubVerticalSpacing);
287  layout->StartRow(0, kHeaderColumnSetId);
288  layout->AddView(create_shortcuts_label_);
289
290  layout->AddPaddingRow(0, views::kLabelToControlVerticalSpacing);
291  layout->StartRow(0, kTableColumnSetId);
292  layout->AddView(desktop_check_box_);
293
294  if (menu_check_box_ != NULL) {
295    layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing);
296    layout->StartRow(0, kTableColumnSetId);
297    layout->AddView(menu_check_box_);
298  }
299
300  if (quick_launch_check_box_ != NULL) {
301    layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing);
302    layout->StartRow(0, kTableColumnSetId);
303    layout->AddView(quick_launch_check_box_);
304  }
305}
306
307gfx::Size CreateApplicationShortcutView::GetPreferredSize() {
308  // TODO(evanm): should this use IDS_CREATE_SHORTCUTS_DIALOG_WIDTH_CHARS?
309  static const int kDialogWidth = 360;
310  int height = GetLayoutManager()->GetPreferredHeightForWidth(this,
311      kDialogWidth);
312  return gfx::Size(kDialogWidth, height);
313}
314
315std::wstring CreateApplicationShortcutView::GetDialogButtonLabel(
316    MessageBoxFlags::DialogButton button) const {
317  if (button == MessageBoxFlags::DIALOGBUTTON_OK) {
318    return UTF16ToWide(l10n_util::GetStringUTF16(IDS_CREATE_SHORTCUTS_COMMIT));
319  }
320
321  return std::wstring();
322}
323
324bool CreateApplicationShortcutView::IsDialogButtonEnabled(
325    MessageBoxFlags::DialogButton button) const {
326  if (button == MessageBoxFlags::DIALOGBUTTON_OK)
327    return desktop_check_box_->checked() ||
328           ((menu_check_box_ != NULL) &&
329            menu_check_box_->checked()) ||
330           ((quick_launch_check_box_ != NULL) &&
331            quick_launch_check_box_->checked());
332
333  return true;
334}
335
336bool CreateApplicationShortcutView::CanResize() const {
337  return false;
338}
339
340bool CreateApplicationShortcutView::CanMaximize() const {
341  return false;
342}
343
344bool CreateApplicationShortcutView::IsAlwaysOnTop() const {
345  return false;
346}
347
348bool CreateApplicationShortcutView::HasAlwaysOnTopMenu() const {
349  return false;
350}
351
352bool CreateApplicationShortcutView::IsModal() const {
353  return true;
354}
355
356std::wstring CreateApplicationShortcutView::GetWindowTitle() const {
357  return UTF16ToWide(l10n_util::GetStringUTF16(IDS_CREATE_SHORTCUTS_TITLE));
358}
359
360bool CreateApplicationShortcutView::Accept() {
361  if (!IsDialogButtonEnabled(MessageBoxFlags::DIALOGBUTTON_OK))
362    return false;
363
364  shortcut_info_.create_on_desktop = desktop_check_box_->checked();
365  shortcut_info_.create_in_applications_menu = menu_check_box_ == NULL ? false :
366      menu_check_box_->checked();
367
368#if defined(OS_WIN)
369  shortcut_info_.create_in_quick_launch_bar = quick_launch_check_box_ == NULL ?
370      NULL : quick_launch_check_box_->checked();
371#elif defined(OS_POSIX)
372  // Create shortcut in Mac dock or as Linux (gnome/kde) application launcher
373  // are not implemented yet.
374  shortcut_info_.create_in_quick_launch_bar = false;
375#endif
376
377  web_app::CreateShortcut(profile_->GetPath(),
378                          shortcut_info_,
379                          NULL);
380  return true;
381}
382
383
384views::View* CreateApplicationShortcutView::GetContentsView() {
385  return this;
386}
387
388views::Checkbox* CreateApplicationShortcutView::AddCheckbox(
389    const std::wstring& text, bool checked) {
390  views::Checkbox* checkbox = new views::Checkbox(text);
391  checkbox->SetChecked(checked);
392  checkbox->set_listener(this);
393  return checkbox;
394}
395
396void CreateApplicationShortcutView::ButtonPressed(views::Button* sender,
397                                                  const views::Event& event) {
398  if (sender == desktop_check_box_)
399    profile_->GetPrefs()->SetBoolean(prefs::kWebAppCreateOnDesktop,
400        desktop_check_box_->checked() ? true : false);
401  else if (sender == menu_check_box_)
402    profile_->GetPrefs()->SetBoolean(prefs::kWebAppCreateInAppsMenu,
403        menu_check_box_->checked() ? true : false);
404  else if (sender == quick_launch_check_box_)
405    profile_->GetPrefs()->SetBoolean(prefs::kWebAppCreateInQuickLaunchBar,
406        quick_launch_check_box_->checked() ? true : false);
407
408  // When no checkbox is checked we should not have the action button enabled.
409  GetDialogClientView()->UpdateDialogButtons();
410}
411
412CreateUrlApplicationShortcutView::CreateUrlApplicationShortcutView(
413    TabContentsWrapper* tab_contents)
414    : CreateApplicationShortcutView(tab_contents->profile()),
415      tab_contents_(tab_contents),
416      pending_download_(NULL)  {
417
418  web_app::GetShortcutInfoForTab(tab_contents_, &shortcut_info_);
419  const WebApplicationInfo& app_info =
420      tab_contents_->extension_tab_helper()->web_app_info();
421  if (!app_info.icons.empty()) {
422    web_app::GetIconsInfo(app_info, &unprocessed_icons_);
423    FetchIcon();
424  }
425
426  InitControls();
427}
428
429CreateUrlApplicationShortcutView::~CreateUrlApplicationShortcutView() {
430  if (pending_download_)
431    pending_download_->Cancel();
432}
433
434bool CreateUrlApplicationShortcutView::Accept() {
435  if (!CreateApplicationShortcutView::Accept())
436    return false;
437
438  tab_contents_->extension_tab_helper()->SetAppIcon(shortcut_info_.favicon);
439  if (tab_contents_->tab_contents()->delegate()) {
440    tab_contents_->tab_contents()->delegate()->ConvertContentsToApplication(
441        tab_contents_->tab_contents());
442  }
443  return true;
444}
445
446void CreateUrlApplicationShortcutView::FetchIcon() {
447  // There should only be fetch job at a time.
448  DCHECK(pending_download_ == NULL);
449
450  if (unprocessed_icons_.empty())  // No icons to fetch.
451    return;
452
453  pending_download_ = new IconDownloadCallbackFunctor(this);
454  DCHECK(pending_download_);
455
456  tab_contents_->tab_contents()->favicon_helper().DownloadImage(
457      unprocessed_icons_.back().url,
458      std::max(unprocessed_icons_.back().width,
459               unprocessed_icons_.back().height),
460      history::FAVICON,
461      NewCallback(pending_download_, &IconDownloadCallbackFunctor::Run));
462
463  unprocessed_icons_.pop_back();
464}
465
466void CreateUrlApplicationShortcutView::OnIconDownloaded(bool errored,
467                                                        const SkBitmap& image) {
468  pending_download_ = NULL;
469
470  if (!errored && !image.isNull()) {
471    shortcut_info_.favicon = image;
472    static_cast<AppInfoView*>(app_info_)->UpdateIcon(shortcut_info_.favicon);
473  } else {
474    FetchIcon();
475  }
476}
477
478CreateChromeApplicationShortcutView::CreateChromeApplicationShortcutView(
479    Profile* profile,
480    const Extension* app) :
481      CreateApplicationShortcutView(profile),
482      app_(app),
483      ALLOW_THIS_IN_INITIALIZER_LIST(tracker_(this)) {
484
485  shortcut_info_.extension_id = app_->id();
486  shortcut_info_.url = GURL(app_->launch_web_url());
487  shortcut_info_.title = UTF8ToUTF16(app_->name());
488  shortcut_info_.description = UTF8ToUTF16(app_->description());
489
490  // The icon will be resized to |max_size|.
491  const gfx::Size max_size(kAppIconSize, kAppIconSize);
492
493  // Look for an icon.  If there is no icon at the ideal size,
494  // we will resize whatever we can get.  Making a large icon smaller
495  // is prefered to making a small icon larger, so look for a larger
496  // icon first:
497  ExtensionResource icon_resource = app_->GetIconResource(
498      kAppIconSize,
499      ExtensionIconSet::MATCH_BIGGER);
500
501  // If no icon exists that is the desired size or larger, get the
502  // largest icon available:
503  if (icon_resource.empty()) {
504    icon_resource = app_->GetIconResource(
505        kAppIconSize,
506        ExtensionIconSet::MATCH_SMALLER);
507  }
508
509  tracker_.LoadImage(app_,
510                     icon_resource,
511                     max_size,
512                     ImageLoadingTracker::DONT_CACHE);
513
514  InitControls();
515}
516
517CreateChromeApplicationShortcutView::~CreateChromeApplicationShortcutView() {}
518
519// Called by tracker_ when the app's icon is loaded.
520void CreateChromeApplicationShortcutView::OnImageLoaded(
521    SkBitmap* image, const ExtensionResource& resource, int index) {
522  if (image->isNull()) {
523    NOTREACHED() << "Corrupt image in profile?";
524    return;
525  }
526  shortcut_info_.favicon = *image;
527  static_cast<AppInfoView*>(app_info_)->UpdateIcon(shortcut_info_.favicon);
528}
529
530