 Chromium Code Reviews
 Chromium Code Reviews Issue 9443007:
  Add Chrome To Mobile Service and Views Page Action.  (Closed) 
  Base URL: svn://svn.chromium.org/chrome/trunk/src
    
  
    Issue 9443007:
  Add Chrome To Mobile Service and Views Page Action.  (Closed) 
  Base URL: svn://svn.chromium.org/chrome/trunk/src| Index: chrome/browser/ui/views/chrome_to_mobile_bubble_view.cc | 
| diff --git a/chrome/browser/ui/views/chrome_to_mobile_bubble_view.cc b/chrome/browser/ui/views/chrome_to_mobile_bubble_view.cc | 
| new file mode 100755 | 
| index 0000000000000000000000000000000000000000..417cc4959926820c2f87886d68cea59e22518889 | 
| --- /dev/null | 
| +++ b/chrome/browser/ui/views/chrome_to_mobile_bubble_view.cc | 
| @@ -0,0 +1,323 @@ | 
| +// Copyright (c) 2012 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/chrome_to_mobile_bubble_view.h" | 
| + | 
| +#include "base/bind.h" | 
| +#include "base/file_util.h" | 
| +#include "base/string16.h" | 
| +#include "base/utf_string_conversions.h" | 
| +#include "base/values.h" | 
| +#include "chrome/app/chrome_command_ids.h" | 
| +#include "chrome/browser/browser_process.h" | 
| +#include "chrome/browser/chrome_to_mobile_service.h" | 
| +#include "chrome/browser/chrome_to_mobile_service_factory.h" | 
| +#include "chrome/browser/profiles/profile.h" | 
| +#include "chrome/browser/ui/browser.h" | 
| +#include "chrome/browser/ui/browser_list.h" | 
| +#include "chrome/browser/ui/views/window.h" | 
| +#include "content/public/browser/browser_thread.h" | 
| +#include "content/public/browser/web_contents.h" | 
| +#include "grit/generated_resources.h" | 
| +#include "grit/theme_resources.h" | 
| +#include "ui/base/animation/throb_animation.h" | 
| +#include "ui/base/keycodes/keyboard_codes.h" | 
| +#include "ui/base/l10n/l10n_util.h" | 
| +#include "ui/base/resource/resource_bundle.h" | 
| +#include "ui/base/text/bytes_formatting.h" | 
| +#include "ui/views/controls/button/checkbox.h" | 
| +#include "ui/views/controls/button/radio_button.h" | 
| +#include "ui/views/controls/button/text_button.h" | 
| +#include "ui/views/controls/label.h" | 
| +#include "ui/views/events/event.h" | 
| +#include "ui/views/layout/grid_layout.h" | 
| +#include "ui/views/layout/layout_constants.h" | 
| + | 
| +using views::GridLayout; | 
| + | 
| +namespace { | 
| + | 
| +// The millisecond interval between refreshes of the "Sending..." progress text. | 
| +const size_t kProgressThrobIntervalMS = 300; | 
| + | 
| +// The bubble's margin for the "Sending..." and "Sent" states. | 
| +const size_t kProgressMargin = 20; | 
| + | 
| +// The snapshot path constant; combined with a guid to store the MHTML snapshot. | 
| +const FilePath::CharType kSnapshotPath[] = | 
| + FILE_PATH_LITERAL("chrome_to_mobile_snapshot_.mhtml"); | 
| + | 
| +void DeleteFilePath(const FilePath& file_path) { | 
| + bool success = file_util::Delete(file_path, false); | 
| + DCHECK(success); | 
| +} | 
| + | 
| +} // namespace | 
| + | 
| +// Declared in browser_dialogs.h so callers don't have to depend on our header. | 
| + | 
| +namespace browser { | 
| + | 
| +void ShowChromeToMobileBubbleView(views::View* anchor_view, Profile* profile) { | 
| + ChromeToMobileBubbleView::ShowBubble(anchor_view, profile); | 
| +} | 
| + | 
| +void HideChromeToMobileBubbleView() { | 
| + ChromeToMobileBubbleView::Hide(); | 
| +} | 
| + | 
| +bool IsChromeToMobileBubbleViewShowing() { | 
| + return ChromeToMobileBubbleView::IsShowing(); | 
| +} | 
| + | 
| +} // namespace browser | 
| + | 
| +// ChromeToMobileBubbleView ---------------------------------------------------- | 
| + | 
| +ChromeToMobileBubbleView* ChromeToMobileBubbleView::bubble_ = NULL; | 
| + | 
| +// static | 
| +void ChromeToMobileBubbleView::ShowBubble(views::View* anchor_view, | 
| + Profile* profile) { | 
| + if (IsShowing()) | 
| + return; | 
| + | 
| + bubble_ = new ChromeToMobileBubbleView(anchor_view, profile); | 
| + browser::CreateViewsBubble(bubble_); | 
| + bubble_->Show(); | 
| +} | 
| + | 
| +// static | 
| +bool ChromeToMobileBubbleView::IsShowing() { | 
| + return bubble_ != NULL; | 
| +} | 
| + | 
| +void ChromeToMobileBubbleView::Hide() { | 
| + if (IsShowing()) | 
| + bubble_->GetWidget()->Close(); | 
| +} | 
| + | 
| +ChromeToMobileBubbleView::~ChromeToMobileBubbleView() {} | 
| + | 
| +views::View* ChromeToMobileBubbleView::GetInitiallyFocusedView() { | 
| + return send_; | 
| +} | 
| + | 
| +gfx::Rect ChromeToMobileBubbleView::GetAnchorRect() { | 
| + // Compensate for some built-in padding in the arrow image. | 
| + gfx::Rect rect(BubbleDelegateView::GetAnchorRect()); | 
| + rect.Inset(0, anchor_view() ? 5 : 0); | 
| + return rect; | 
| +} | 
| + | 
| +void ChromeToMobileBubbleView::WindowClosing() { | 
| + // We have to reset |bubble_| here, not in our destructor, because we'll be | 
| + // destroyed asynchronously and the shown state will be checked before then. | 
| + DCHECK(bubble_ == this); | 
| + bubble_ = NULL; | 
| + | 
| + if (!snapshot_path_.empty()) | 
| + content::BrowserThread::PostBlockingPoolTask(FROM_HERE, | 
| 
sky
2012/03/11 21:41:22
I'm not familiar with this, does it black the UI t
 
msw
2012/03/12 10:17:18
Nope, See the decl; this is the new FILE thread pa
 | 
| + base::Bind(&DeleteFilePath, snapshot_path_)); | 
| +} | 
| + | 
| +bool ChromeToMobileBubbleView::AcceleratorPressed( | 
| + const ui::Accelerator& accelerator) { | 
| + if (accelerator.key_code() == ui::VKEY_RETURN && | 
| + (send_->HasFocus() || cancel_->HasFocus())) { | 
| + HandleButtonPressed(send_->HasFocus() ? send_ : cancel_); | 
| + return true; | 
| + } | 
| + return BubbleDelegateView::AcceleratorPressed(accelerator); | 
| +} | 
| + | 
| +void ChromeToMobileBubbleView::OnSendComplete(bool success) { | 
| + progress_animation_->Stop(); | 
| + progress_label_->SetText(l10n_util::GetStringUTF16(success ? | 
| + IDS_CHROME_TO_MOBILE_BUBBLE_SENT : IDS_CHROME_TO_MOBILE_BUBBLE_ERROR)); | 
| + Layout(); | 
| +} | 
| + | 
| +void ChromeToMobileBubbleView::AnimationProgressed( | 
| + const ui::Animation* animation) { | 
| + if (animation == progress_animation_.get()) { | 
| + // Show the "Sending" string with 0-3 trailing periods "..." | 
| + progress_periods_count_ = (progress_periods_count_ + 1) % 4; | 
| + string16 text = progress_label_->GetText(); | 
| + if (progress_periods_count_ == 0) | 
| + progress_label_->SetText(text.substr(0, text.length() - 3)); | 
| + else | 
| + progress_label_->SetText(text.append(ASCIIToUTF16("."))); | 
| 
sky
2012/03/11 21:41:22
I suspect that adding a '.' to the end of the stri
 
msw
2012/03/12 10:17:18
I'm now using multiple IDS messages for internatio
 | 
| + // Run Layout now but do not resize the bubble when adding/removing periods. | 
| + Layout(); | 
| + return; | 
| + } | 
| + views::BubbleDelegateView::AnimationProgressed(animation); | 
| +} | 
| + | 
| +void ChromeToMobileBubbleView::SnapshotGenerated(const FilePath& snapshot_path, | 
| + int64 snapshot_bytes) { | 
| + snapshot_path_ = snapshot_path; | 
| + send_copy_->SetText( | 
| + l10n_util::GetStringFUTF16(IDS_CHROME_TO_MOBILE_BUBBLE_SEND_COPY, | 
| + ui::FormatBytes(snapshot_bytes))); | 
| + send_copy_->SetEnabled(snapshot_bytes > 0); | 
| + Layout(); | 
| +} | 
| + | 
| +void ChromeToMobileBubbleView::Init() { | 
| + GridLayout* layout = new GridLayout(this); | 
| + SetLayoutManager(layout); | 
| + | 
| + const size_t single_column_set_id = 0; | 
| + views::ColumnSet* cs = layout->AddColumnSet(single_column_set_id); | 
| + cs->AddColumn(GridLayout::LEADING, GridLayout::LEADING, 0, | 
| + GridLayout::USE_PREF, 0, 0); | 
| + cs->AddPaddingColumn(1, 0); | 
| + | 
| + const size_t button_column_set_id = 1; | 
| + cs = layout->AddColumnSet(button_column_set_id); | 
| + cs->AddPaddingColumn(1, 0); | 
| + cs->AddColumn(GridLayout::LEADING, GridLayout::TRAILING, 0, | 
| + GridLayout::USE_PREF, 0, 0); | 
| + // Subtract 2px for the natural button padding and to correspond with row | 
| + // separation height; like BookmarkBubbleView. | 
| + cs->AddPaddingColumn(0, views::kRelatedButtonHSpacing - 2); | 
| + cs->AddColumn(GridLayout::LEADING, GridLayout::TRAILING, 0, | 
| + GridLayout::USE_PREF, 0, 0); | 
| + | 
| + std::vector<DictionaryValue*> mobiles = | 
| + ChromeToMobileServiceFactory::GetForProfile(profile_)->mobiles(); | 
| + DCHECK_GT(mobiles.size(), 0U); | 
| + layout->StartRow(0, single_column_set_id); | 
| + if (mobiles.size() == 1) { | 
| + selected_mobile_ = mobiles[0]; | 
| + string16 mobile_name; | 
| + mobiles[0]->GetString("name", &mobile_name); | 
| + layout->AddView(new views::Label( | 
| + l10n_util::GetStringFUTF16(IDS_CHROME_TO_MOBILE_BUBBLE_SINGLE_TITLE, | 
| + mobile_name))); | 
| + } else { | 
| + layout->AddView(new views::Label( | 
| + l10n_util::GetStringUTF16(IDS_CHROME_TO_MOBILE_BUBBLE_MULTI_TITLE))); | 
| + | 
| + const size_t radio_column_set_id = 2; | 
| + cs = layout->AddColumnSet(radio_column_set_id); | 
| + cs->AddPaddingColumn(0, views::kRelatedControlHorizontalSpacing); | 
| + cs->AddColumn(GridLayout::LEADING, GridLayout::CENTER, 0, | 
| + GridLayout::USE_PREF, 0, 0); | 
| + | 
| + views::RadioButton* radio; | 
| + layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing); | 
| + for (std::vector<DictionaryValue*>::const_iterator it = mobiles.begin(); | 
| + it != mobiles.end(); ++it) { | 
| + string16 name; | 
| + (*it)->GetString("name", &name); | 
| + radio = new views::RadioButton(name, 0); | 
| + radio->set_listener(this); | 
| + mobile_map_[radio] = *it; | 
| + layout->StartRow(0, radio_column_set_id); | 
| + layout->AddView(radio); | 
| + } | 
| + mobile_map_.begin()->first->SetChecked(true); | 
| + selected_mobile_ = mobile_map_.begin()->second; | 
| + } | 
| + | 
| + send_copy_ = new views::Checkbox( | 
| + l10n_util::GetStringFUTF16(IDS_CHROME_TO_MOBILE_BUBBLE_SEND_COPY, | 
| + l10n_util::GetStringUTF16( | 
| + IDS_CHROME_TO_MOBILE_BUBBLE_SEND_COPY_GENERATING))); | 
| + send_copy_->SetEnabled(false); | 
| + layout->StartRow(0, single_column_set_id); | 
| + layout->AddView(send_copy_); | 
| + | 
| + layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing); | 
| + send_ = new views::NativeTextButton( | 
| + this, l10n_util::GetStringUTF16(IDS_CHROME_TO_MOBILE_BUBBLE_SEND)); | 
| + send_->SetIsDefault(true); | 
| + cancel_ = new views::NativeTextButton( | 
| + this, l10n_util::GetStringUTF16(IDS_CANCEL)); | 
| + layout->StartRow(0, button_column_set_id); | 
| + layout->AddView(send_); | 
| + layout->AddView(cancel_); | 
| + | 
| + AddAccelerator(ui::Accelerator(ui::VKEY_RETURN, 0)); | 
| +} | 
| + | 
| +ChromeToMobileBubbleView::ChromeToMobileBubbleView(views::View* anchor_view, | 
| + Profile* profile) | 
| + : BubbleDelegateView(anchor_view, views::BubbleBorder::TOP_RIGHT), | 
| + ALLOW_THIS_IN_INITIALIZER_LIST(weak_ptr_factory_(this)), | 
| + profile_(profile), | 
| + snapshot_path_(kSnapshotPath), | 
| + send_copy_(NULL), | 
| + send_(NULL), | 
| + cancel_(NULL), | 
| + progress_label_(NULL), | 
| + progress_periods_count_(0) { | 
| + // Generate the MHTML snapshot immediately to report its size in the bubble. | 
| + content::WebContents* web_contents = | 
| + BrowserList::GetLastActiveWithProfile(profile_)->GetSelectedWebContents(); | 
| + web_contents->GenerateMHTML( | 
| + FilePath(kSnapshotPath).InsertBeforeExtensionASCII(guid::GenerateGUID()), | 
| + base::Bind(&ChromeToMobileBubbleView::SnapshotGenerated, | 
| + weak_ptr_factory_.GetWeakPtr())); | 
| +} | 
| + | 
| +void ChromeToMobileBubbleView::ButtonPressed(views::Button* sender, | 
| + const views::Event& event) { | 
| + HandleButtonPressed(sender); | 
| +} | 
| + | 
| +void ChromeToMobileBubbleView::HandleButtonPressed(views::Button* sender) { | 
| + if (sender == send_) { | 
| + Send(); | 
| + } else if (sender == cancel_) { | 
| + GetWidget()->Close(); | 
| + } else { | 
| + // The sender is a mobile radio button | 
| + views::RadioButton* radio = static_cast<views::RadioButton*>(sender); | 
| + DCHECK(mobile_map_.find(radio) != mobile_map_.end()); | 
| + selected_mobile_ = mobile_map_.find(radio)->second; | 
| + } | 
| +} | 
| + | 
| +void ChromeToMobileBubbleView::Send() { | 
| + string16 mobile_id; | 
| + selected_mobile_->GetString("id", &mobile_id); | 
| + content::WebContents* web_contents = | 
| + BrowserList::GetLastActiveWithProfile(profile_)->GetSelectedWebContents(); | 
| + ChromeToMobileServiceFactory::GetForProfile(profile_)->SendToMobile( | 
| + mobile_id, web_contents->GetURL(), web_contents->GetTitle(), | 
| + send_copy_->checked() ? snapshot_path_ : FilePath(), | 
| + weak_ptr_factory_.GetWeakPtr()); | 
| + // Pass ownership of the temp MHTML file to the service, if sending a copy. | 
| + if (send_copy_->checked()) | 
| + snapshot_path_.clear(); | 
| + | 
| + // Re-initialize the view's contents to show progress sending the page. | 
| + RemoveAllChildViews(true); | 
| 
sky
2012/03/11 21:41:22
Reset the fields that point to views this deletes.
 
msw
2012/03/12 10:17:18
Done.
 | 
| + GridLayout* layout = new GridLayout(this); | 
| + SetLayoutManager(layout); | 
| + | 
| + const size_t single_column_set_id = 0; | 
| + views::ColumnSet* cs = layout->AddColumnSet(single_column_set_id); | 
| + cs->AddColumn(GridLayout::LEADING, GridLayout::LEADING, 0, | 
| + GridLayout::USE_PREF, 0, 0); | 
| + set_margin(kProgressMargin); | 
| + | 
| + // Start the string off with its three periods to properly size the bubble. | 
| + layout->StartRow(0, single_column_set_id); | 
| + progress_periods_count_ = 3; | 
| + progress_label_ = new views::Label( | 
| + l10n_util::GetStringUTF16(IDS_CHROME_TO_MOBILE_BUBBLE_SENDING).append( | 
| + ASCIIToUTF16("..."))); | 
| + layout->AddView(progress_label_); | 
| + SizeToContents(); | 
| + | 
| + progress_animation_.reset(new ui::ThrobAnimation(this)); | 
| + progress_animation_->set_timer_interval( | 
| 
sky
2012/03/11 21:41:22
I think you want SetDuration and don't add set_tim
 
msw
2012/03/12 10:17:18
Done. Would a RepeatingTimer be better than an Ani
 
sky
2012/03/12 15:28:12
Either one works, but we typically use animation f
 | 
| + base::TimeDelta::FromMilliseconds(kProgressThrobIntervalMS)); | 
| + progress_animation_->StartThrobbing(-1); | 
| +} |