| // 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 "ash/system/drive/tray_drive.h" |
| |
| #include <vector> |
| |
| #include "ash/metrics/user_metrics_recorder.h" |
| #include "ash/shell.h" |
| #include "ash/system/tray/fixed_sized_scroll_view.h" |
| #include "ash/system/tray/hover_highlight_view.h" |
| #include "ash/system/tray/system_tray.h" |
| #include "ash/system/tray/system_tray_delegate.h" |
| #include "ash/system/tray/system_tray_notifier.h" |
| #include "ash/system/tray/tray_constants.h" |
| #include "ash/system/tray/tray_details_view.h" |
| #include "ash/system/tray/tray_item_more.h" |
| #include "ash/system/tray/tray_item_view.h" |
| #include "base/logging.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "grit/ash_resources.h" |
| #include "grit/ash_strings.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/font.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/views/controls/button/image_button.h" |
| #include "ui/views/controls/image_view.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/controls/progress_bar.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/grid_layout.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace ash { |
| |
| namespace internal { |
| |
| namespace { |
| |
| const int kSidePadding = 8; |
| const int kHorizontalPadding = 6; |
| const int kVerticalPadding = 6; |
| const int kTopPadding = 6; |
| const int kBottomPadding = 10; |
| const int kProgressBarWidth = 100; |
| const int kProgressBarHeight = 11; |
| const int64 kHideDelayInMs = 1000; |
| |
| base::string16 GetTrayLabel(const ash::DriveOperationStatusList& list) { |
| return l10n_util::GetStringFUTF16(IDS_ASH_STATUS_TRAY_DRIVE_SYNCING, |
| base::IntToString16(static_cast<int>(list.size()))); |
| } |
| |
| scoped_ptr<ash::DriveOperationStatusList> GetCurrentOperationList() { |
| ash::SystemTrayDelegate* delegate = |
| ash::Shell::GetInstance()->system_tray_delegate(); |
| scoped_ptr<ash::DriveOperationStatusList> list( |
| new ash::DriveOperationStatusList); |
| delegate->GetDriveOperationStatusList(list.get()); |
| return list.Pass(); |
| } |
| |
| } |
| |
| namespace tray { |
| |
| class DriveDefaultView : public TrayItemMore { |
| public: |
| DriveDefaultView(SystemTrayItem* owner, |
| const DriveOperationStatusList* list) |
| : TrayItemMore(owner, true) { |
| ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance(); |
| |
| SetImage(bundle.GetImageNamed(IDR_AURA_UBER_TRAY_DRIVE).ToImageSkia()); |
| Update(list); |
| } |
| |
| virtual ~DriveDefaultView() {} |
| |
| void Update(const DriveOperationStatusList* list) { |
| DCHECK(list); |
| base::string16 label = GetTrayLabel(*list); |
| SetLabel(label); |
| SetAccessibleName(label); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(DriveDefaultView); |
| }; |
| |
| class DriveDetailedView : public TrayDetailsView, |
| public ViewClickListener { |
| public: |
| DriveDetailedView(SystemTrayItem* owner, |
| const DriveOperationStatusList* list) |
| : TrayDetailsView(owner), |
| settings_(NULL), |
| in_progress_img_(NULL), |
| done_img_(NULL), |
| failed_img_(NULL) { |
| in_progress_img_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed( |
| IDR_AURA_UBER_TRAY_DRIVE); |
| done_img_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed( |
| IDR_AURA_UBER_TRAY_DRIVE_DONE); |
| failed_img_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed( |
| IDR_AURA_UBER_TRAY_DRIVE_FAILED); |
| |
| Update(list); |
| } |
| |
| virtual ~DriveDetailedView() { |
| STLDeleteValues(&update_map_); |
| } |
| |
| void Update(const DriveOperationStatusList* list) { |
| AppendOperationList(list); |
| AppendSettings(); |
| AppendHeaderEntry(list); |
| |
| SchedulePaint(); |
| } |
| |
| private: |
| |
| class OperationProgressBar : public views::ProgressBar { |
| public: |
| OperationProgressBar() {} |
| private: |
| |
| // Overridden from View: |
| virtual gfx::Size GetPreferredSize() OVERRIDE { |
| return gfx::Size(kProgressBarWidth, kProgressBarHeight); |
| } |
| |
| DISALLOW_COPY_AND_ASSIGN(OperationProgressBar); |
| }; |
| |
| class RowView : public HoverHighlightView, |
| public views::ButtonListener { |
| public: |
| RowView(DriveDetailedView* parent, |
| ash::DriveOperationStatus::OperationState state, |
| double progress, |
| const base::FilePath& file_path, |
| int32 operation_id) |
| : HoverHighlightView(parent), |
| container_(parent), |
| status_img_(NULL), |
| label_container_(NULL), |
| progress_bar_(NULL), |
| cancel_button_(NULL), |
| operation_id_(operation_id) { |
| // Status image. |
| status_img_ = new views::ImageView(); |
| AddChildView(status_img_); |
| |
| label_container_ = new views::View(); |
| label_container_->SetLayoutManager(new views::BoxLayout( |
| views::BoxLayout::kVertical, 0, 0, kVerticalPadding)); |
| #if defined(OS_POSIX) |
| base::string16 file_label = UTF8ToUTF16(file_path.BaseName().value()); |
| #elif defined(OS_WIN) |
| base::string16 file_label = WideToUTF16(file_path.BaseName().value()); |
| #endif |
| views::Label* label = new views::Label(file_label); |
| label->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| label_container_->AddChildView(label); |
| // Add progress bar. |
| progress_bar_ = new OperationProgressBar(); |
| label_container_->AddChildView(progress_bar_); |
| |
| AddChildView(label_container_); |
| |
| cancel_button_ = new views::ImageButton(this); |
| cancel_button_->SetImage(views::ImageButton::STATE_NORMAL, |
| ResourceBundle::GetSharedInstance().GetImageSkiaNamed( |
| IDR_AURA_UBER_TRAY_DRIVE_CANCEL)); |
| cancel_button_->SetImage(views::ImageButton::STATE_HOVERED, |
| ResourceBundle::GetSharedInstance().GetImageSkiaNamed( |
| IDR_AURA_UBER_TRAY_DRIVE_CANCEL_HOVER)); |
| |
| UpdateStatus(state, progress); |
| AddChildView(cancel_button_); |
| } |
| |
| void UpdateStatus(ash::DriveOperationStatus::OperationState state, |
| double progress) { |
| status_img_->SetImage(container_->GetImageForState(state)); |
| progress_bar_->SetValue(progress); |
| cancel_button_->SetVisible( |
| state == ash::DriveOperationStatus::OPERATION_NOT_STARTED || |
| state == ash::DriveOperationStatus::OPERATION_IN_PROGRESS); |
| } |
| |
| private: |
| |
| // views::View overrides. |
| virtual gfx::Size GetPreferredSize() OVERRIDE { |
| return gfx::Size( |
| status_img_->GetPreferredSize().width() + |
| label_container_->GetPreferredSize().width() + |
| cancel_button_->GetPreferredSize().width() + |
| 2 * kSidePadding + 2 * kHorizontalPadding, |
| std::max(status_img_->GetPreferredSize().height(), |
| std::max(label_container_->GetPreferredSize().height(), |
| cancel_button_->GetPreferredSize().height())) + |
| kTopPadding + kBottomPadding); |
| } |
| |
| virtual void Layout() OVERRIDE { |
| gfx::Rect child_area(GetLocalBounds()); |
| if (child_area.IsEmpty()) |
| return; |
| |
| int pos_x = child_area.x() + kSidePadding; |
| int pos_y = child_area.y() + kTopPadding; |
| |
| gfx::Rect bounds_status( |
| gfx::Point(pos_x, |
| pos_y + (child_area.height() - kTopPadding - |
| kBottomPadding - |
| status_img_->GetPreferredSize().height())/2), |
| status_img_->GetPreferredSize()); |
| status_img_->SetBoundsRect( |
| gfx::IntersectRects(bounds_status, child_area)); |
| pos_x += status_img_->bounds().width() + kHorizontalPadding; |
| |
| gfx::Rect bounds_label(pos_x, |
| pos_y, |
| child_area.width() - 2 * kSidePadding - |
| 2 * kHorizontalPadding - |
| status_img_->GetPreferredSize().width() - |
| cancel_button_->GetPreferredSize().width(), |
| label_container_->GetPreferredSize().height()); |
| label_container_->SetBoundsRect( |
| gfx::IntersectRects(bounds_label, child_area)); |
| pos_x += label_container_->bounds().width() + kHorizontalPadding; |
| |
| gfx::Rect bounds_button( |
| gfx::Point(pos_x, |
| pos_y + (child_area.height() - kTopPadding - |
| kBottomPadding - |
| cancel_button_->GetPreferredSize().height())/2), |
| cancel_button_->GetPreferredSize()); |
| cancel_button_->SetBoundsRect( |
| gfx::IntersectRects(bounds_button, child_area)); |
| } |
| |
| // views::ButtonListener overrides. |
| virtual void ButtonPressed(views::Button* sender, |
| const ui::Event& event) OVERRIDE { |
| DCHECK(sender == cancel_button_); |
| Shell::GetInstance()->metrics()->RecordUserMetricsAction( |
| ash::UMA_STATUS_AREA_DRIVE_CANCEL_OPERATION); |
| container_->OnCancelOperation(operation_id_); |
| } |
| |
| DriveDetailedView* container_; |
| views::ImageView* status_img_; |
| views::View* label_container_; |
| views::ProgressBar* progress_bar_; |
| views::ImageButton* cancel_button_; |
| int32 operation_id_; |
| |
| DISALLOW_COPY_AND_ASSIGN(RowView); |
| }; |
| |
| void AppendHeaderEntry(const DriveOperationStatusList* list) { |
| if (footer()) |
| return; |
| CreateSpecialRow(IDS_ASH_STATUS_TRAY_DRIVE, this); |
| } |
| |
| gfx::ImageSkia* GetImageForState( |
| ash::DriveOperationStatus::OperationState state) { |
| switch (state) { |
| case ash::DriveOperationStatus::OPERATION_NOT_STARTED: |
| case ash::DriveOperationStatus::OPERATION_IN_PROGRESS: |
| return in_progress_img_; |
| case ash::DriveOperationStatus::OPERATION_COMPLETED: |
| return done_img_; |
| case ash::DriveOperationStatus::OPERATION_FAILED: |
| return failed_img_; |
| } |
| return failed_img_; |
| } |
| |
| void OnCancelOperation(int32 operation_id) { |
| SystemTrayDelegate* delegate = Shell::GetInstance()->system_tray_delegate(); |
| delegate->CancelDriveOperation(operation_id); |
| } |
| |
| void AppendOperationList(const DriveOperationStatusList* list) { |
| if (!scroller()) |
| CreateScrollableList(); |
| |
| // Apply the update. |
| std::set<base::FilePath> new_set; |
| bool item_list_changed = false; |
| for (DriveOperationStatusList::const_iterator it = list->begin(); |
| it != list->end(); ++it) { |
| const DriveOperationStatus& operation = *it; |
| |
| new_set.insert(operation.file_path); |
| std::map<base::FilePath, RowView*>::iterator existing_item = |
| update_map_.find(operation.file_path); |
| |
| if (existing_item != update_map_.end()) { |
| existing_item->second->UpdateStatus(operation.state, |
| operation.progress); |
| } else { |
| RowView* row_view = new RowView(this, |
| operation.state, |
| operation.progress, |
| operation.file_path, |
| operation.id); |
| |
| update_map_[operation.file_path] = row_view; |
| scroll_content()->AddChildView(row_view); |
| item_list_changed = true; |
| } |
| } |
| |
| // Remove items from the list that haven't been added or modified with this |
| // update batch. |
| std::set<base::FilePath> remove_set; |
| for (std::map<base::FilePath, RowView*>::iterator update_iter = |
| update_map_.begin(); |
| update_iter != update_map_.end(); ++update_iter) { |
| if (new_set.find(update_iter->first) == new_set.end()) { |
| remove_set.insert(update_iter->first); |
| } |
| } |
| |
| for (std::set<base::FilePath>::iterator removed_iter = remove_set.begin(); |
| removed_iter != remove_set.end(); ++removed_iter) { |
| delete update_map_[*removed_iter]; |
| update_map_.erase(*removed_iter); |
| item_list_changed = true; |
| } |
| |
| if (item_list_changed) |
| scroller()->Layout(); |
| |
| // Close the details if there is really nothing to show there anymore. |
| if (new_set.empty() && GetWidget()) |
| GetWidget()->Close(); |
| } |
| |
| void AppendSettings() { |
| if (settings_) |
| return; |
| |
| HoverHighlightView* container = new HoverHighlightView(this); |
| container->AddLabel(ui::ResourceBundle::GetSharedInstance(). |
| GetLocalizedString(IDS_ASH_STATUS_TRAY_DRIVE_SETTINGS), |
| gfx::Font::NORMAL); |
| AddChildView(container); |
| settings_ = container; |
| } |
| |
| // Overridden from ViewClickListener. |
| virtual void OnViewClicked(views::View* sender) OVERRIDE { |
| SystemTrayDelegate* delegate = Shell::GetInstance()->system_tray_delegate(); |
| if (sender == footer()->content()) { |
| TransitionToDefaultView(); |
| } else if (sender == settings_) { |
| delegate->ShowDriveSettings(); |
| } |
| } |
| |
| // Maps operation entries to their file paths. |
| std::map<base::FilePath, RowView*> update_map_; |
| views::View* settings_; |
| gfx::ImageSkia* in_progress_img_; |
| gfx::ImageSkia* done_img_; |
| gfx::ImageSkia* failed_img_; |
| |
| DISALLOW_COPY_AND_ASSIGN(DriveDetailedView); |
| }; |
| |
| } // namespace tray |
| |
| TrayDrive::TrayDrive(SystemTray* system_tray) : |
| TrayImageItem(system_tray, IDR_AURA_UBER_TRAY_DRIVE_LIGHT), |
| default_(NULL), |
| detailed_(NULL) { |
| Shell::GetInstance()->system_tray_notifier()->AddDriveObserver(this); |
| } |
| |
| TrayDrive::~TrayDrive() { |
| Shell::GetInstance()->system_tray_notifier()->RemoveDriveObserver(this); |
| } |
| |
| bool TrayDrive::GetInitialVisibility() { |
| return false; |
| } |
| |
| views::View* TrayDrive::CreateDefaultView(user::LoginStatus status) { |
| DCHECK(!default_); |
| |
| if (status != user::LOGGED_IN_USER && status != user::LOGGED_IN_OWNER) |
| return NULL; |
| |
| // If the list is empty AND the tray icon is invisible (= not in the margin |
| // duration of delayed item hiding), don't show the item. |
| scoped_ptr<DriveOperationStatusList> list(GetCurrentOperationList()); |
| if (list->empty() && !tray_view()->visible()) |
| return NULL; |
| |
| default_ = new tray::DriveDefaultView(this, list.get()); |
| return default_; |
| } |
| |
| views::View* TrayDrive::CreateDetailedView(user::LoginStatus status) { |
| DCHECK(!detailed_); |
| |
| if (status != user::LOGGED_IN_USER && status != user::LOGGED_IN_OWNER) |
| return NULL; |
| |
| // If the list is empty AND the tray icon is invisible (= not in the margin |
| // duration of delayed item hiding), don't show the item. |
| scoped_ptr<DriveOperationStatusList> list(GetCurrentOperationList()); |
| if (list->empty() && !tray_view()->visible()) |
| return NULL; |
| |
| Shell::GetInstance()->metrics()->RecordUserMetricsAction( |
| ash::UMA_STATUS_AREA_DETAILED_DRIVE_VIEW); |
| detailed_ = new tray::DriveDetailedView(this, list.get()); |
| return detailed_; |
| } |
| |
| void TrayDrive::DestroyDefaultView() { |
| default_ = NULL; |
| } |
| |
| void TrayDrive::DestroyDetailedView() { |
| detailed_ = NULL; |
| } |
| |
| void TrayDrive::UpdateAfterLoginStatusChange(user::LoginStatus status) { |
| if (status == user::LOGGED_IN_USER || status == user::LOGGED_IN_OWNER) |
| return; |
| |
| tray_view()->SetVisible(false); |
| DestroyDefaultView(); |
| DestroyDetailedView(); |
| } |
| |
| void TrayDrive::OnDriveJobUpdated(const DriveOperationStatus& status) { |
| // The Drive job list manager changed its notification interface *not* to send |
| // the whole list of operations each time, to clarify which operation is |
| // updated and to reduce redundancy. |
| // |
| // TrayDrive should be able to benefit from the change, but for now, to |
| // incrementally migrate to the new way with minimum diffs, we still get the |
| // list of operations each time the event is fired. |
| // TODO(kinaba) http://crbug.com/128079 clean it up. |
| scoped_ptr<DriveOperationStatusList> list(GetCurrentOperationList()); |
| bool is_new_item = true; |
| for (size_t i = 0; i < list->size(); ++i) { |
| if ((*list)[i].id == status.id) { |
| (*list)[i] = status; |
| is_new_item = false; |
| break; |
| } |
| } |
| if (is_new_item) |
| list->push_back(status); |
| |
| // Check if all the operations are in the finished state. |
| bool all_jobs_finished = true; |
| for (size_t i = 0; i < list->size(); ++i) { |
| if ((*list)[i].state != DriveOperationStatus::OPERATION_COMPLETED && |
| (*list)[i].state != DriveOperationStatus::OPERATION_FAILED) { |
| all_jobs_finished = false; |
| break; |
| } |
| } |
| |
| if (all_jobs_finished) { |
| // If all the jobs ended, the tray item will be hidden after a certain |
| // amount of delay. This is to avoid flashes between sequentially executed |
| // Drive operations (see crbug/165679). |
| hide_timer_.Start(FROM_HERE, |
| base::TimeDelta::FromMilliseconds(kHideDelayInMs), |
| this, |
| &TrayDrive::HideIfNoOperations); |
| return; |
| } |
| |
| // If the list is non-empty, stop the hiding timer (if any). |
| hide_timer_.Stop(); |
| |
| tray_view()->SetVisible(true); |
| if (default_) |
| default_->Update(list.get()); |
| if (detailed_) |
| detailed_->Update(list.get()); |
| } |
| |
| void TrayDrive::HideIfNoOperations() { |
| DriveOperationStatusList empty_list; |
| |
| tray_view()->SetVisible(false); |
| if (default_) |
| default_->Update(&empty_list); |
| if (detailed_) |
| detailed_->Update(&empty_list); |
| } |
| |
| } // namespace internal |
| } // namespace ash |