| // Copyright 2018 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/bluetooth/bluetooth_detailed_view.h" |
| |
| #include <map> |
| #include <memory> |
| #include <utility> |
| |
| #include "ash/resources/vector_icons/vector_icons.h" |
| #include "ash/shell.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "ash/system/model/system_tray_model.h" |
| #include "ash/system/tray/hover_highlight_view.h" |
| #include "ash/system/tray/tray_info_label.h" |
| #include "ash/system/tray/tray_popup_item_style.h" |
| #include "ash/system/tray/tray_popup_utils.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "services/device/public/cpp/bluetooth/bluetooth_utils.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "ui/views/controls/button/toggle_button.h" |
| #include "ui/views/controls/image_view.h" |
| #include "ui/views/controls/scroll_view.h" |
| #include "ui/views/layout/box_layout.h" |
| |
| using device::mojom::BluetoothSystem; |
| using device::mojom::BluetoothDeviceInfo; |
| |
| namespace ash { |
| namespace tray { |
| namespace { |
| |
| const int kDisabledPanelLabelBaselineY = 20; |
| |
| // Returns corresponding device type icons for given Bluetooth device types and |
| // connection states. |
| const gfx::VectorIcon& GetBluetoothDeviceIcon( |
| BluetoothDeviceInfo::DeviceType device_type, |
| BluetoothDeviceInfo::ConnectionState connection_state) { |
| switch (device_type) { |
| case BluetoothDeviceInfo::DeviceType::kComputer: |
| return ash::kSystemMenuComputerIcon; |
| case BluetoothDeviceInfo::DeviceType::kPhone: |
| return ash::kSystemMenuPhoneIcon; |
| case BluetoothDeviceInfo::DeviceType::kAudio: |
| case BluetoothDeviceInfo::DeviceType::kCarAudio: |
| return ash::kSystemMenuHeadsetIcon; |
| case BluetoothDeviceInfo::DeviceType::kVideo: |
| return ash::kSystemMenuVideocamIcon; |
| case BluetoothDeviceInfo::DeviceType::kJoystick: |
| case BluetoothDeviceInfo::DeviceType::kGamepad: |
| return ash::kSystemMenuGamepadIcon; |
| case BluetoothDeviceInfo::DeviceType::kKeyboard: |
| case BluetoothDeviceInfo::DeviceType::kKeyboardMouseCombo: |
| return ash::kSystemMenuKeyboardIcon; |
| case BluetoothDeviceInfo::DeviceType::kTablet: |
| return ash::kSystemMenuTabletIcon; |
| case BluetoothDeviceInfo::DeviceType::kMouse: |
| return ash::kSystemMenuMouseIcon; |
| case BluetoothDeviceInfo::DeviceType::kModem: |
| case BluetoothDeviceInfo::DeviceType::kPeripheral: |
| return ash::kSystemMenuBluetoothIcon; |
| default: |
| return connection_state == |
| BluetoothDeviceInfo::ConnectionState::kConnected |
| ? ash::kSystemMenuBluetoothConnectedIcon |
| : ash::kSystemMenuBluetoothIcon; |
| } |
| } |
| |
| views::View* CreateDisabledPanel() { |
| views::View* container = new views::View; |
| auto box_layout = |
| std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical); |
| box_layout->set_main_axis_alignment( |
| views::BoxLayout::MAIN_AXIS_ALIGNMENT_CENTER); |
| container->SetLayoutManager(std::move(box_layout)); |
| |
| TrayPopupItemStyle style(TrayPopupItemStyle::FontStyle::DETAILED_VIEW_LABEL); |
| style.set_color_style(TrayPopupItemStyle::ColorStyle::DISABLED); |
| |
| views::ImageView* image_view = new views::ImageView; |
| image_view->SetImage(gfx::CreateVectorIcon(kSystemMenuBluetoothDisabledIcon, |
| style.GetIconColor())); |
| image_view->SetVerticalAlignment(views::ImageView::TRAILING); |
| container->AddChildView(image_view); |
| |
| views::Label* label = new views::Label( |
| l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_BLUETOOTH_DISABLED)); |
| style.SetupLabel(label); |
| label->SetBorder(views::CreateEmptyBorder( |
| kDisabledPanelLabelBaselineY - label->GetBaseline(), 0, 0, 0)); |
| container->AddChildView(label); |
| |
| // Make top padding of the icon equal to the height of the label so that the |
| // icon is vertically aligned to center of the container. |
| image_view->SetBorder( |
| views::CreateEmptyBorder(label->GetPreferredSize().height(), 0, 0, 0)); |
| return container; |
| } |
| |
| } // namespace |
| |
| BluetoothDetailedView::BluetoothDetailedView(DetailedViewDelegate* delegate, |
| LoginStatus login) |
| : TrayDetailedView(delegate), |
| login_(login), |
| toggle_(nullptr), |
| settings_(nullptr), |
| disabled_panel_(nullptr) { |
| CreateItems(); |
| } |
| |
| BluetoothDetailedView::~BluetoothDetailedView() = default; |
| |
| void BluetoothDetailedView::ShowLoadingIndicator() { |
| // Setting a value of -1 gives progress_bar an infinite-loading behavior. |
| ShowProgress(-1, true); |
| } |
| |
| void BluetoothDetailedView::HideLoadingIndicator() { |
| ShowProgress(0, false); |
| } |
| |
| void BluetoothDetailedView::ShowBluetoothDisabledPanel() { |
| device_map_.clear(); |
| scroll_content()->RemoveAllChildViews(true); |
| |
| DCHECK(scroller()); |
| if (!disabled_panel_) { |
| disabled_panel_ = CreateDisabledPanel(); |
| // Insert |disabled_panel_| before the scroller, since the scroller will |
| // have unnecessary bottom border when it is not the last child. |
| AddChildViewAt(disabled_panel_, GetIndexOf(scroller())); |
| // |disabled_panel_| need to fill the remaining space below the title row |
| // so that the inner contents of |disabled_panel_| are placed properly. |
| box_layout()->SetFlexForView(disabled_panel_, 1); |
| } |
| |
| disabled_panel_->SetVisible(true); |
| scroller()->SetVisible(false); |
| |
| Layout(); |
| } |
| |
| void BluetoothDetailedView::HideBluetoothDisabledPanel() { |
| DCHECK(scroller()); |
| if (disabled_panel_) |
| disabled_panel_->SetVisible(false); |
| scroller()->SetVisible(true); |
| |
| Layout(); |
| } |
| |
| bool BluetoothDetailedView::IsDeviceScrollListEmpty() const { |
| return device_map_.empty(); |
| } |
| |
| void BluetoothDetailedView::UpdateDeviceScrollList( |
| const BluetoothDeviceList& connected_devices, |
| const BluetoothDeviceList& connecting_devices, |
| const BluetoothDeviceList& paired_not_connected_devices, |
| const BluetoothDeviceList& discovered_not_paired_devices) { |
| connecting_devices_.clear(); |
| for (const auto& device : connecting_devices) |
| connecting_devices_.push_back(device->Clone()); |
| |
| paired_not_connected_devices_.clear(); |
| for (const auto& device : paired_not_connected_devices) |
| paired_not_connected_devices_.push_back(device->Clone()); |
| |
| base::Optional<BluetoothAddress> focused_device_address = |
| GetFocusedDeviceAddress(); |
| |
| device_map_.clear(); |
| scroll_content()->RemoveAllChildViews(true); |
| |
| // Add paired devices and their section header to the list. |
| bool has_paired_devices = !connected_devices.empty() || |
| !connecting_devices.empty() || |
| !paired_not_connected_devices.empty(); |
| if (has_paired_devices) { |
| AddScrollListSubHeader(IDS_ASH_STATUS_TRAY_BLUETOOTH_PAIRED_DEVICES); |
| AppendSameTypeDevicesToScrollList(connected_devices, true, true); |
| AppendSameTypeDevicesToScrollList(connecting_devices, true, false); |
| AppendSameTypeDevicesToScrollList(paired_not_connected_devices, false, |
| false); |
| } |
| |
| // Add unpaired devices to the list. If at least one paired device is |
| // present, also add a section header above the unpaired devices. |
| if (!discovered_not_paired_devices.empty()) { |
| if (has_paired_devices) |
| AddScrollListSubHeader(IDS_ASH_STATUS_TRAY_BLUETOOTH_UNPAIRED_DEVICES); |
| AppendSameTypeDevicesToScrollList(discovered_not_paired_devices, false, |
| false); |
| } |
| |
| // Show user Bluetooth state if there is no bluetooth devices in list. |
| if (device_map_.empty()) { |
| scroll_content()->AddChildView(new TrayInfoLabel( |
| nullptr /* delegate */, IDS_ASH_STATUS_TRAY_BLUETOOTH_DISCOVERING)); |
| } |
| |
| // Focus the device which was focused before the device-list update. |
| if (focused_device_address) |
| FocusDeviceByAddress(focused_device_address.value()); |
| |
| scroll_content()->InvalidateLayout(); |
| |
| Layout(); |
| } |
| |
| void BluetoothDetailedView::SetToggleIsOn(bool is_on) { |
| if (toggle_) |
| toggle_->SetIsOn(is_on, true); |
| } |
| |
| void BluetoothDetailedView::CreateItems() { |
| CreateScrollableList(); |
| CreateTitleRow(IDS_ASH_STATUS_TRAY_BLUETOOTH); |
| } |
| |
| void BluetoothDetailedView::AppendSameTypeDevicesToScrollList( |
| const BluetoothDeviceList& list, |
| bool highlight, |
| bool checked) { |
| for (const auto& device : list) { |
| const gfx::VectorIcon& icon = |
| GetBluetoothDeviceIcon(device->device_type, device->connection_state); |
| HoverHighlightView* container = AddScrollListItem( |
| icon, device::GetBluetoothDeviceNameForDisplay(device)); |
| switch (device->connection_state) { |
| case BluetoothDeviceInfo::ConnectionState::kNotConnected: |
| break; |
| case BluetoothDeviceInfo::ConnectionState::kConnecting: |
| SetupConnectingScrollListItem(container); |
| break; |
| case BluetoothDeviceInfo::ConnectionState::kConnected: |
| SetupConnectedScrollListItem(container); |
| break; |
| } |
| device_map_[container] = device->address; |
| } |
| } |
| |
| bool BluetoothDetailedView::FoundDevice( |
| const BluetoothAddress& device_address, |
| const BluetoothDeviceList& device_list) const { |
| for (const auto& device : device_list) { |
| if (device->address == device_address) |
| return true; |
| } |
| return false; |
| } |
| |
| void BluetoothDetailedView::UpdateClickedDevice( |
| const BluetoothAddress& device_address, |
| views::View* item_container) { |
| if (FoundDevice(device_address, paired_not_connected_devices_)) { |
| HoverHighlightView* container = |
| static_cast<HoverHighlightView*>(item_container); |
| SetupConnectingScrollListItem(container); |
| scroll_content()->SizeToPreferredSize(); |
| scroller()->Layout(); |
| } |
| } |
| |
| void BluetoothDetailedView::ShowSettings() { |
| if (TrayPopupUtils::CanOpenWebUISettings()) { |
| Shell::Get()->system_tray_model()->client_ptr()->ShowBluetoothSettings(); |
| CloseBubble(); |
| } |
| } |
| |
| base::Optional<BluetoothAddress> |
| BluetoothDetailedView::GetFocusedDeviceAddress() const { |
| for (const auto& view_and_address : device_map_) { |
| if (view_and_address.first->HasFocus()) |
| return view_and_address.second; |
| } |
| return base::nullopt; |
| } |
| |
| void BluetoothDetailedView::FocusDeviceByAddress( |
| const BluetoothAddress& address) const { |
| for (auto& view_and_address : device_map_) { |
| if (view_and_address.second == address) { |
| view_and_address.first->RequestFocus(); |
| return; |
| } |
| } |
| } |
| |
| void BluetoothDetailedView::HandleViewClicked(views::View* view) { |
| TrayBluetoothHelper* helper = Shell::Get()->tray_bluetooth_helper(); |
| if (helper->GetBluetoothState() != BluetoothSystem::State::kPoweredOn) |
| return; |
| |
| std::map<views::View*, BluetoothAddress>::iterator find; |
| find = device_map_.find(view); |
| if (find == device_map_.end()) |
| return; |
| |
| const BluetoothAddress& device_address = find->second; |
| if (FoundDevice(device_address, connecting_devices_)) |
| return; |
| |
| UpdateClickedDevice(device_address, view); |
| helper->ConnectToBluetoothDevice(device_address); |
| } |
| |
| void BluetoothDetailedView::HandleButtonPressed(views::Button* sender, |
| const ui::Event& event) { |
| if (sender == toggle_) { |
| Shell::Get()->tray_bluetooth_helper()->SetBluetoothEnabled( |
| toggle_->is_on()); |
| } else if (sender == settings_) { |
| ShowSettings(); |
| } else { |
| NOTREACHED(); |
| } |
| } |
| |
| void BluetoothDetailedView::CreateExtraTitleRowButtons() { |
| if (login_ == LoginStatus::LOCKED) |
| return; |
| |
| DCHECK(!toggle_); |
| DCHECK(!settings_); |
| |
| tri_view()->SetContainerVisible(TriView::Container::END, true); |
| |
| toggle_ = |
| TrayPopupUtils::CreateToggleButton(this, IDS_ASH_STATUS_TRAY_BLUETOOTH); |
| toggle_->SetIsOn(Shell::Get()->tray_bluetooth_helper()->GetBluetoothState() == |
| BluetoothSystem::State::kPoweredOn, |
| false /* animate */); |
| tri_view()->AddView(TriView::Container::END, toggle_); |
| |
| settings_ = CreateSettingsButton(IDS_ASH_STATUS_TRAY_BLUETOOTH_SETTINGS); |
| tri_view()->AddView(TriView::Container::END, settings_); |
| } |
| |
| } // namespace tray |
| } // namespace ash |