// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/system/network/network_info_bubble.h"
#include <memory>
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/typography.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/network/tray_network_state_model.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/services/network_config/public/cpp/cros_network_config_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/ui_base_types.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/view.h"
namespace ash {
namespace {
using chromeos::network_config::mojom::DeviceStateProperties;
using chromeos::network_config::mojom::NetworkStateProperties;
using chromeos::network_config::mojom::NetworkType;
// This margin value is used for:
// - Margins inside the border.
// - Horizontal spacing between the border and parent bubble border.
// - Distance between top of the border and the bottom of the anchor view
// (horizontal rule).
constexpr int kBubbleMargin = 8;
// Elevation used for the bubble shadow effect (tiny).
constexpr int kBubbleShadowElevation = 2;
// 00:00:00:00:00:00 is provided when a device MAC address cannot be retrieved.
constexpr char kMissingMacAddress[] = "00:00:00:00:00:00";
std::string ComputeMacAddress(NetworkType network_type) {
const DeviceStateProperties* device =
Shell::Get()->system_tray_model()->network_state_model()->GetDevice(
network_type);
if (!device || !device->mac_address ||
device->mac_address == kMissingMacAddress) {
return std::string();
}
return *device->mac_address;
}
} // namespace
NetworkInfoBubble::NetworkInfoBubble(base::WeakPtr<Delegate> delegate,
views::View* anchor)
: views::BubbleDialogDelegateView(anchor, views::BubbleBorder::TOP_RIGHT),
delegate_(delegate) {
SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
set_margins(gfx::Insets(kBubbleMargin));
SetArrow(views::BubbleBorder::NONE);
set_shadow(views::BubbleBorder::NO_SHADOW);
SetNotifyEnterExitOnChild(true);
SetLayoutManager(std::make_unique<views::FillLayout>());
std::u16string info_text;
label_container_ =
AddChildView(views::Builder<views::BoxLayoutView>()
.SetOrientation(views::BoxLayout::Orientation::kVertical)
.Build());
info_text = ComputeInfoText();
// If the `ComputeInfoText()` is not the no networks info label, it means
// labels are added and no need to add the no network label.
if (info_text.compare(
l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_NO_NETWORKS)) != 0) {
label_container_->SetBorder(
views::CreateEmptyBorder(gfx::Insets::VH(0, 8)));
label_container_->SetID(kNetworkInfoBubbleLabelViewId);
return;
}
std::unique_ptr<views::Label> label = std::make_unique<views::Label>(
info_text.empty() ? ComputeInfoText() : info_text);
label->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);
TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosButton2, *label);
label->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD);
label->SetID(kNetworkInfoBubbleLabelViewId);
label->SetMultiLine(true);
label->SetSelectable(true);
AddChildView(label.release());
}
NetworkInfoBubble::~NetworkInfoBubble() {
if (delegate_) {
delegate_->OnInfoBubbleDestroyed();
}
}
gfx::Size NetworkInfoBubble::CalculatePreferredSize(
const views::SizeBounds& available_size) const {
// This bubble should be inset by kBubbleMargin on the left and right relative
// to the parent bubble.
const gfx::Size anchor_size = GetAnchorView()->size();
int contents_width =
anchor_size.width() - 2 * kBubbleMargin - margins().width();
return gfx::Size(
contents_width,
GetLayoutManager()->GetPreferredHeightForWidth(this, contents_width));
}
void NetworkInfoBubble::OnMouseExited(const ui::MouseEvent& event) {
GetWidget()->Close(); // Deletes |this|.
}
void NetworkInfoBubble::OnBeforeBubbleWidgetInit(
views::Widget::InitParams* params,
views::Widget* widget) const {
params->shadow_type = views::Widget::InitParams::ShadowType::kDrop;
params->shadow_elevation = kBubbleShadowElevation;
params->name = "NetworkInfoBubble";
}
std::u16string NetworkInfoBubble::ComputeInfoText() {
DCHECK(delegate_);
std::u16string info_text;
auto add_address_if_exists = [&info_text](std::string address, int text_id,
views::View* label_container) {
if (address.empty()) {
return;
}
if (!info_text.empty()) {
info_text += u"\n";
}
info_text +=
l10n_util::GetStringFUTF16(text_id, base::UTF8ToUTF16(address));
auto container =
views::Builder<views::BoxLayoutView>()
.SetOrientation(views::BoxLayout::Orientation::kHorizontal)
.Build();
std::unique_ptr<views::Label> title_label = std::make_unique<views::Label>(
l10n_util::GetStringFUTF16(text_id, u""));
title_label->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD);
title_label->SetSelectable(true);
std::unique_ptr<views::Label> address_label =
std::make_unique<views::Label>(base::UTF8ToUTF16(address));
address_label->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD);
address_label->SetSelectable(true);
title_label->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);
TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosButton2,
*title_label);
address_label->SetEnabledColorId(cros_tokens::kCrosSysOnSurfaceVariant);
TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosBody2,
*address_label);
container->AddChildView(title_label.release());
container->AddChildView(address_label.release());
label_container->AddChildView(container.release());
};
const NetworkStateProperties* default_network = Shell::Get()
->system_tray_model()
->network_state_model()
->default_network();
const DeviceStateProperties* device =
default_network
? Shell::Get()->system_tray_model()->network_state_model()->GetDevice(
default_network->type)
: nullptr;
if (device) {
if (device->ipv4_address) {
add_address_if_exists(device->ipv4_address->ToString(),
IDS_ASH_STATUS_TRAY_IP_ADDRESS, label_container_);
}
if (device->ipv6_address) {
add_address_if_exists(device->ipv6_address->ToString(),
IDS_ASH_STATUS_TRAY_IPV6_ADDRESS, label_container_);
}
}
if (delegate_->ShouldIncludeDeviceAddresses()) {
add_address_if_exists(ComputeMacAddress(NetworkType::kEthernet),
IDS_ASH_STATUS_TRAY_ETHERNET_ADDRESS,
label_container_);
add_address_if_exists(ComputeMacAddress(NetworkType::kWiFi),
IDS_ASH_STATUS_TRAY_WIFI_ADDRESS, label_container_);
add_address_if_exists(ComputeMacAddress(NetworkType::kCellular),
IDS_ASH_STATUS_TRAY_CELLULAR_ADDRESS,
label_container_);
}
// Avoid returning an empty bubble when no network information is available.
if (info_text.empty()) {
return l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_NO_NETWORKS);
}
return info_text;
}
BEGIN_METADATA(NetworkInfoBubble)
END_METADATA
} // namespace ash