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