1 // Copyright (c) 2012 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 "ash/system/drive/tray_drive.h" 6 7 #include <vector> 8 9 #include "ash/metrics/user_metrics_recorder.h" 10 #include "ash/shell.h" 11 #include "ash/system/tray/fixed_sized_scroll_view.h" 12 #include "ash/system/tray/hover_highlight_view.h" 13 #include "ash/system/tray/system_tray.h" 14 #include "ash/system/tray/system_tray_delegate.h" 15 #include "ash/system/tray/system_tray_notifier.h" 16 #include "ash/system/tray/tray_constants.h" 17 #include "ash/system/tray/tray_details_view.h" 18 #include "ash/system/tray/tray_item_more.h" 19 #include "ash/system/tray/tray_item_view.h" 20 #include "base/logging.h" 21 #include "base/stl_util.h" 22 #include "base/strings/string_number_conversions.h" 23 #include "base/strings/utf_string_conversions.h" 24 #include "grit/ash_resources.h" 25 #include "grit/ash_strings.h" 26 #include "ui/base/l10n/l10n_util.h" 27 #include "ui/base/resource/resource_bundle.h" 28 #include "ui/gfx/font.h" 29 #include "ui/gfx/image/image.h" 30 #include "ui/views/controls/button/image_button.h" 31 #include "ui/views/controls/image_view.h" 32 #include "ui/views/controls/label.h" 33 #include "ui/views/controls/progress_bar.h" 34 #include "ui/views/layout/box_layout.h" 35 #include "ui/views/layout/grid_layout.h" 36 #include "ui/views/widget/widget.h" 37 38 namespace ash { 39 40 namespace internal { 41 42 namespace { 43 44 const int kSidePadding = 8; 45 const int kHorizontalPadding = 6; 46 const int kVerticalPadding = 6; 47 const int kTopPadding = 6; 48 const int kBottomPadding = 10; 49 const int kProgressBarWidth = 100; 50 const int kProgressBarHeight = 11; 51 const int64 kHideDelayInMs = 1000; 52 53 base::string16 GetTrayLabel(const ash::DriveOperationStatusList& list) { 54 return l10n_util::GetStringFUTF16(IDS_ASH_STATUS_TRAY_DRIVE_SYNCING, 55 base::IntToString16(static_cast<int>(list.size()))); 56 } 57 58 scoped_ptr<ash::DriveOperationStatusList> GetCurrentOperationList() { 59 ash::SystemTrayDelegate* delegate = 60 ash::Shell::GetInstance()->system_tray_delegate(); 61 scoped_ptr<ash::DriveOperationStatusList> list( 62 new ash::DriveOperationStatusList); 63 delegate->GetDriveOperationStatusList(list.get()); 64 return list.Pass(); 65 } 66 67 } 68 69 namespace tray { 70 71 class DriveDefaultView : public TrayItemMore { 72 public: 73 DriveDefaultView(SystemTrayItem* owner, 74 const DriveOperationStatusList* list) 75 : TrayItemMore(owner, true) { 76 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance(); 77 78 SetImage(bundle.GetImageNamed(IDR_AURA_UBER_TRAY_DRIVE).ToImageSkia()); 79 Update(list); 80 } 81 82 virtual ~DriveDefaultView() {} 83 84 void Update(const DriveOperationStatusList* list) { 85 DCHECK(list); 86 base::string16 label = GetTrayLabel(*list); 87 SetLabel(label); 88 SetAccessibleName(label); 89 } 90 91 private: 92 DISALLOW_COPY_AND_ASSIGN(DriveDefaultView); 93 }; 94 95 class DriveDetailedView : public TrayDetailsView, 96 public ViewClickListener { 97 public: 98 DriveDetailedView(SystemTrayItem* owner, 99 const DriveOperationStatusList* list) 100 : TrayDetailsView(owner), 101 settings_(NULL), 102 in_progress_img_(NULL), 103 done_img_(NULL), 104 failed_img_(NULL) { 105 in_progress_img_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 106 IDR_AURA_UBER_TRAY_DRIVE); 107 done_img_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 108 IDR_AURA_UBER_TRAY_DRIVE_DONE); 109 failed_img_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 110 IDR_AURA_UBER_TRAY_DRIVE_FAILED); 111 112 Update(list); 113 } 114 115 virtual ~DriveDetailedView() { 116 STLDeleteValues(&update_map_); 117 } 118 119 void Update(const DriveOperationStatusList* list) { 120 AppendOperationList(list); 121 AppendSettings(); 122 AppendHeaderEntry(list); 123 124 SchedulePaint(); 125 } 126 127 private: 128 129 class OperationProgressBar : public views::ProgressBar { 130 public: 131 OperationProgressBar() {} 132 private: 133 134 // Overridden from View: 135 virtual gfx::Size GetPreferredSize() OVERRIDE { 136 return gfx::Size(kProgressBarWidth, kProgressBarHeight); 137 } 138 139 DISALLOW_COPY_AND_ASSIGN(OperationProgressBar); 140 }; 141 142 class RowView : public HoverHighlightView, 143 public views::ButtonListener { 144 public: 145 RowView(DriveDetailedView* parent, 146 ash::DriveOperationStatus::OperationState state, 147 double progress, 148 const base::FilePath& file_path, 149 int32 operation_id) 150 : HoverHighlightView(parent), 151 container_(parent), 152 status_img_(NULL), 153 label_container_(NULL), 154 progress_bar_(NULL), 155 cancel_button_(NULL), 156 operation_id_(operation_id) { 157 // Status image. 158 status_img_ = new views::ImageView(); 159 AddChildView(status_img_); 160 161 label_container_ = new views::View(); 162 label_container_->SetLayoutManager(new views::BoxLayout( 163 views::BoxLayout::kVertical, 0, 0, kVerticalPadding)); 164 #if defined(OS_POSIX) 165 base::string16 file_label = UTF8ToUTF16(file_path.BaseName().value()); 166 #elif defined(OS_WIN) 167 base::string16 file_label = WideToUTF16(file_path.BaseName().value()); 168 #endif 169 views::Label* label = new views::Label(file_label); 170 label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 171 label_container_->AddChildView(label); 172 // Add progress bar. 173 progress_bar_ = new OperationProgressBar(); 174 label_container_->AddChildView(progress_bar_); 175 176 AddChildView(label_container_); 177 178 cancel_button_ = new views::ImageButton(this); 179 cancel_button_->SetImage(views::ImageButton::STATE_NORMAL, 180 ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 181 IDR_AURA_UBER_TRAY_DRIVE_CANCEL)); 182 cancel_button_->SetImage(views::ImageButton::STATE_HOVERED, 183 ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 184 IDR_AURA_UBER_TRAY_DRIVE_CANCEL_HOVER)); 185 186 UpdateStatus(state, progress); 187 AddChildView(cancel_button_); 188 } 189 190 void UpdateStatus(ash::DriveOperationStatus::OperationState state, 191 double progress) { 192 status_img_->SetImage(container_->GetImageForState(state)); 193 progress_bar_->SetValue(progress); 194 cancel_button_->SetVisible( 195 state == ash::DriveOperationStatus::OPERATION_NOT_STARTED || 196 state == ash::DriveOperationStatus::OPERATION_IN_PROGRESS); 197 } 198 199 private: 200 201 // views::View overrides. 202 virtual gfx::Size GetPreferredSize() OVERRIDE { 203 return gfx::Size( 204 status_img_->GetPreferredSize().width() + 205 label_container_->GetPreferredSize().width() + 206 cancel_button_->GetPreferredSize().width() + 207 2 * kSidePadding + 2 * kHorizontalPadding, 208 std::max(status_img_->GetPreferredSize().height(), 209 std::max(label_container_->GetPreferredSize().height(), 210 cancel_button_->GetPreferredSize().height())) + 211 kTopPadding + kBottomPadding); 212 } 213 214 virtual void Layout() OVERRIDE { 215 gfx::Rect child_area(GetLocalBounds()); 216 if (child_area.IsEmpty()) 217 return; 218 219 int pos_x = child_area.x() + kSidePadding; 220 int pos_y = child_area.y() + kTopPadding; 221 222 gfx::Rect bounds_status( 223 gfx::Point(pos_x, 224 pos_y + (child_area.height() - kTopPadding - 225 kBottomPadding - 226 status_img_->GetPreferredSize().height())/2), 227 status_img_->GetPreferredSize()); 228 status_img_->SetBoundsRect( 229 gfx::IntersectRects(bounds_status, child_area)); 230 pos_x += status_img_->bounds().width() + kHorizontalPadding; 231 232 gfx::Rect bounds_label(pos_x, 233 pos_y, 234 child_area.width() - 2 * kSidePadding - 235 2 * kHorizontalPadding - 236 status_img_->GetPreferredSize().width() - 237 cancel_button_->GetPreferredSize().width(), 238 label_container_->GetPreferredSize().height()); 239 label_container_->SetBoundsRect( 240 gfx::IntersectRects(bounds_label, child_area)); 241 pos_x += label_container_->bounds().width() + kHorizontalPadding; 242 243 gfx::Rect bounds_button( 244 gfx::Point(pos_x, 245 pos_y + (child_area.height() - kTopPadding - 246 kBottomPadding - 247 cancel_button_->GetPreferredSize().height())/2), 248 cancel_button_->GetPreferredSize()); 249 cancel_button_->SetBoundsRect( 250 gfx::IntersectRects(bounds_button, child_area)); 251 } 252 253 // views::ButtonListener overrides. 254 virtual void ButtonPressed(views::Button* sender, 255 const ui::Event& event) OVERRIDE { 256 DCHECK(sender == cancel_button_); 257 Shell::GetInstance()->metrics()->RecordUserMetricsAction( 258 ash::UMA_STATUS_AREA_DRIVE_CANCEL_OPERATION); 259 container_->OnCancelOperation(operation_id_); 260 } 261 262 DriveDetailedView* container_; 263 views::ImageView* status_img_; 264 views::View* label_container_; 265 views::ProgressBar* progress_bar_; 266 views::ImageButton* cancel_button_; 267 int32 operation_id_; 268 269 DISALLOW_COPY_AND_ASSIGN(RowView); 270 }; 271 272 void AppendHeaderEntry(const DriveOperationStatusList* list) { 273 if (footer()) 274 return; 275 CreateSpecialRow(IDS_ASH_STATUS_TRAY_DRIVE, this); 276 } 277 278 gfx::ImageSkia* GetImageForState( 279 ash::DriveOperationStatus::OperationState state) { 280 switch (state) { 281 case ash::DriveOperationStatus::OPERATION_NOT_STARTED: 282 case ash::DriveOperationStatus::OPERATION_IN_PROGRESS: 283 return in_progress_img_; 284 case ash::DriveOperationStatus::OPERATION_COMPLETED: 285 return done_img_; 286 case ash::DriveOperationStatus::OPERATION_FAILED: 287 return failed_img_; 288 } 289 return failed_img_; 290 } 291 292 void OnCancelOperation(int32 operation_id) { 293 SystemTrayDelegate* delegate = Shell::GetInstance()->system_tray_delegate(); 294 delegate->CancelDriveOperation(operation_id); 295 } 296 297 void AppendOperationList(const DriveOperationStatusList* list) { 298 if (!scroller()) 299 CreateScrollableList(); 300 301 // Apply the update. 302 std::set<base::FilePath> new_set; 303 bool item_list_changed = false; 304 for (DriveOperationStatusList::const_iterator it = list->begin(); 305 it != list->end(); ++it) { 306 const DriveOperationStatus& operation = *it; 307 308 new_set.insert(operation.file_path); 309 std::map<base::FilePath, RowView*>::iterator existing_item = 310 update_map_.find(operation.file_path); 311 312 if (existing_item != update_map_.end()) { 313 existing_item->second->UpdateStatus(operation.state, 314 operation.progress); 315 } else { 316 RowView* row_view = new RowView(this, 317 operation.state, 318 operation.progress, 319 operation.file_path, 320 operation.id); 321 322 update_map_[operation.file_path] = row_view; 323 scroll_content()->AddChildView(row_view); 324 item_list_changed = true; 325 } 326 } 327 328 // Remove items from the list that haven't been added or modified with this 329 // update batch. 330 std::set<base::FilePath> remove_set; 331 for (std::map<base::FilePath, RowView*>::iterator update_iter = 332 update_map_.begin(); 333 update_iter != update_map_.end(); ++update_iter) { 334 if (new_set.find(update_iter->first) == new_set.end()) { 335 remove_set.insert(update_iter->first); 336 } 337 } 338 339 for (std::set<base::FilePath>::iterator removed_iter = remove_set.begin(); 340 removed_iter != remove_set.end(); ++removed_iter) { 341 delete update_map_[*removed_iter]; 342 update_map_.erase(*removed_iter); 343 item_list_changed = true; 344 } 345 346 if (item_list_changed) 347 scroller()->Layout(); 348 349 // Close the details if there is really nothing to show there anymore. 350 if (new_set.empty() && GetWidget()) 351 GetWidget()->Close(); 352 } 353 354 void AppendSettings() { 355 if (settings_) 356 return; 357 358 HoverHighlightView* container = new HoverHighlightView(this); 359 container->AddLabel(ui::ResourceBundle::GetSharedInstance(). 360 GetLocalizedString(IDS_ASH_STATUS_TRAY_DRIVE_SETTINGS), 361 gfx::Font::NORMAL); 362 AddChildView(container); 363 settings_ = container; 364 } 365 366 // Overridden from ViewClickListener. 367 virtual void OnViewClicked(views::View* sender) OVERRIDE { 368 SystemTrayDelegate* delegate = Shell::GetInstance()->system_tray_delegate(); 369 if (sender == footer()->content()) { 370 TransitionToDefaultView(); 371 } else if (sender == settings_) { 372 delegate->ShowDriveSettings(); 373 } 374 } 375 376 // Maps operation entries to their file paths. 377 std::map<base::FilePath, RowView*> update_map_; 378 views::View* settings_; 379 gfx::ImageSkia* in_progress_img_; 380 gfx::ImageSkia* done_img_; 381 gfx::ImageSkia* failed_img_; 382 383 DISALLOW_COPY_AND_ASSIGN(DriveDetailedView); 384 }; 385 386 } // namespace tray 387 388 TrayDrive::TrayDrive(SystemTray* system_tray) : 389 TrayImageItem(system_tray, IDR_AURA_UBER_TRAY_DRIVE_LIGHT), 390 default_(NULL), 391 detailed_(NULL) { 392 Shell::GetInstance()->system_tray_notifier()->AddDriveObserver(this); 393 } 394 395 TrayDrive::~TrayDrive() { 396 Shell::GetInstance()->system_tray_notifier()->RemoveDriveObserver(this); 397 } 398 399 bool TrayDrive::GetInitialVisibility() { 400 return false; 401 } 402 403 views::View* TrayDrive::CreateDefaultView(user::LoginStatus status) { 404 DCHECK(!default_); 405 406 if (status != user::LOGGED_IN_USER && status != user::LOGGED_IN_OWNER) 407 return NULL; 408 409 // If the list is empty AND the tray icon is invisible (= not in the margin 410 // duration of delayed item hiding), don't show the item. 411 scoped_ptr<DriveOperationStatusList> list(GetCurrentOperationList()); 412 if (list->empty() && !tray_view()->visible()) 413 return NULL; 414 415 default_ = new tray::DriveDefaultView(this, list.get()); 416 return default_; 417 } 418 419 views::View* TrayDrive::CreateDetailedView(user::LoginStatus status) { 420 DCHECK(!detailed_); 421 422 if (status != user::LOGGED_IN_USER && status != user::LOGGED_IN_OWNER) 423 return NULL; 424 425 // If the list is empty AND the tray icon is invisible (= not in the margin 426 // duration of delayed item hiding), don't show the item. 427 scoped_ptr<DriveOperationStatusList> list(GetCurrentOperationList()); 428 if (list->empty() && !tray_view()->visible()) 429 return NULL; 430 431 Shell::GetInstance()->metrics()->RecordUserMetricsAction( 432 ash::UMA_STATUS_AREA_DETAILED_DRIVE_VIEW); 433 detailed_ = new tray::DriveDetailedView(this, list.get()); 434 return detailed_; 435 } 436 437 void TrayDrive::DestroyDefaultView() { 438 default_ = NULL; 439 } 440 441 void TrayDrive::DestroyDetailedView() { 442 detailed_ = NULL; 443 } 444 445 void TrayDrive::UpdateAfterLoginStatusChange(user::LoginStatus status) { 446 if (status == user::LOGGED_IN_USER || status == user::LOGGED_IN_OWNER) 447 return; 448 449 tray_view()->SetVisible(false); 450 DestroyDefaultView(); 451 DestroyDetailedView(); 452 } 453 454 void TrayDrive::OnDriveJobUpdated(const DriveOperationStatus& status) { 455 // The Drive job list manager changed its notification interface *not* to send 456 // the whole list of operations each time, to clarify which operation is 457 // updated and to reduce redundancy. 458 // 459 // TrayDrive should be able to benefit from the change, but for now, to 460 // incrementally migrate to the new way with minimum diffs, we still get the 461 // list of operations each time the event is fired. 462 // TODO(kinaba) http://crbug.com/128079 clean it up. 463 scoped_ptr<DriveOperationStatusList> list(GetCurrentOperationList()); 464 bool is_new_item = true; 465 for (size_t i = 0; i < list->size(); ++i) { 466 if ((*list)[i].id == status.id) { 467 (*list)[i] = status; 468 is_new_item = false; 469 break; 470 } 471 } 472 if (is_new_item) 473 list->push_back(status); 474 475 // Check if all the operations are in the finished state. 476 bool all_jobs_finished = true; 477 for (size_t i = 0; i < list->size(); ++i) { 478 if ((*list)[i].state != DriveOperationStatus::OPERATION_COMPLETED && 479 (*list)[i].state != DriveOperationStatus::OPERATION_FAILED) { 480 all_jobs_finished = false; 481 break; 482 } 483 } 484 485 if (all_jobs_finished) { 486 // If all the jobs ended, the tray item will be hidden after a certain 487 // amount of delay. This is to avoid flashes between sequentially executed 488 // Drive operations (see crbug/165679). 489 hide_timer_.Start(FROM_HERE, 490 base::TimeDelta::FromMilliseconds(kHideDelayInMs), 491 this, 492 &TrayDrive::HideIfNoOperations); 493 return; 494 } 495 496 // If the list is non-empty, stop the hiding timer (if any). 497 hide_timer_.Stop(); 498 499 tray_view()->SetVisible(true); 500 if (default_) 501 default_->Update(list.get()); 502 if (detailed_) 503 detailed_->Update(list.get()); 504 } 505 506 void TrayDrive::HideIfNoOperations() { 507 DriveOperationStatusList empty_list; 508 509 tray_view()->SetVisible(false); 510 if (default_) 511 default_->Update(&empty_list); 512 if (detailed_) 513 detailed_->Update(&empty_list); 514 } 515 516 } // namespace internal 517 } // namespace ash 518