chromium/ash/capture_mode/key_combo_view.cc

// Copyright 2022 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/capture_mode/key_combo_view.h"

#include <iterator>
#include <memory>
#include <string>
#include <vector>

#include "ash/accelerators/keyboard_code_util.h"
#include "ash/capture_mode/key_item_view.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/view.h"

namespace ash {

namespace {

constexpr auto kBetweenKeyItemSpace = 8;

// Returns the vector icons for keys that have icons on the keyboard.
const gfx::VectorIcon* GetVectorIconForDemoTools(ui::KeyboardCode key_code) {
  switch (key_code) {
    case ui::VKEY_COMMAND:
      return GetSearchOrLauncherVectorIcon();
    case ui::VKEY_ASSISTANT:
      return &kCaptureModeDemoToolsAssistantIcon;
    case ui::VKEY_SETTINGS:
      return &kCaptureModeDemoToolsMenuIcon;
    default:
      return GetVectorIconForKeyboardCode(key_code);
  }
}

std::unique_ptr<KeyItemView> CreateKeyItemView(ui::KeyboardCode key_code) {
  std::unique_ptr key_item_view = std::make_unique<KeyItemView>(key_code);
  const gfx::VectorIcon* vector_icon = GetVectorIconForDemoTools(key_code);
  if (vector_icon) {
    key_item_view->SetIcon(*vector_icon);
  } else {
    std::u16string key_item_string =
        GetStringForKeyboardCode(key_code, /*remap_positional_key=*/false);
    key_item_view->SetText(key_item_string);
  }
  return key_item_view;
}

ui::KeyboardCode DecodeModifier(int modifier) {
  switch (modifier) {
    case ui::EF_CONTROL_DOWN:
      return ui::VKEY_CONTROL;
    case ui::EF_ALT_DOWN:
      return ui::VKEY_MENU;
    case ui::EF_SHIFT_DOWN:
      return ui::VKEY_SHIFT;
    case ui::EF_COMMAND_DOWN:
      return ui::VKEY_COMMAND;
    default:
      return ui::VKEY_UNKNOWN;
  }
}

bool operator>(const ui::KeyboardCode lhs, const ui::KeyboardCode rhs) {
  auto digitize_modifier_key_code = [&](ui::KeyboardCode key_code) {
    switch (key_code) {
      case ui::VKEY_CONTROL:
        return 0;
      case ui::VKEY_MENU:
        return 1;
      case ui::VKEY_SHIFT:
        return 2;
      case ui::VKEY_COMMAND:
        return 3;
      default:
        return 1000;
    }
  };

  return digitize_modifier_key_code(lhs) > digitize_modifier_key_code(rhs);
}

}  // namespace

// -----------------------------------------------------------------------------
// ModifiersContainerView:

// The container view that hosts the modifiers key item views.
class ModifiersContainerView : public views::View {
  METADATA_HEADER(ModifiersContainerView, views::View)

 public:
  explicit ModifiersContainerView() {
    views::BoxLayout* layout_manager =
        SetLayoutManager(std::make_unique<views::BoxLayout>(
            views::BoxLayout::Orientation::kHorizontal, gfx::Insets::VH(0, 0),
            kBetweenKeyItemSpace));
    layout_manager->set_cross_axis_alignment(
        views::BoxLayout::CrossAxisAlignment::kCenter);
  }

  ModifiersContainerView(const ModifiersContainerView&) = delete;
  ModifiersContainerView& operator=(const ModifiersContainerView&) = delete;
  ~ModifiersContainerView() override = default;

  void RebuildModifiersContainerView(int new_modifiers) {
    // Use XOR to filter out the modifiers that changed, i.e. the 1s in the
    // `diff` bit fields that correspond to the modifiers that changed.
    const int diff = current_modifiers_ ^ new_modifiers;
    for (const auto modifier : {ui::EF_CONTROL_DOWN, ui::EF_ALT_DOWN,
                                ui::EF_SHIFT_DOWN, ui::EF_COMMAND_DOWN}) {
      if ((modifier & diff) == 0) {
        continue;
      }

      const ui::KeyboardCode key_code = DecodeModifier(modifier);

      // Use AND to decide whether we want to do adding or removal operation. If
      // this `modifier` is set in the `new_modifiers` that means it has been
      // recently pressed, which means a new `KeyItemView` needs to be added.
      // Otherwise, it means a key has been recently released, and we should
      // remove its reoccoresponding `KeyItemView`.
      if ((modifier & new_modifiers) != 0) {
        AddModifier(key_code);
      } else {
        RemoveModifier(key_code);
      }
    }

    current_modifiers_ = new_modifiers;
  }

 private:
  void RemoveModifier(ui::KeyboardCode key_code) {
    for (views::View* child : children()) {
      if (static_cast<KeyItemView*>(child)->key_code() == key_code) {
        RemoveChildViewT(child);
        return;
      }
    }
  }

  void AddModifier(ui::KeyboardCode key_code) {
    // We're trying to find the view whose `key_code()` is greater than the
    // given `key_code` so that we can insert a new `KeyItemView` at that index,
    // or at the end if no such view was found. This keeps the modifiers sorted
    // as `Ctrl < Alt < Shift < Search`.
    const auto iter =
        base::ranges::find_if(children(), [key_code](views::View* child) {
          return static_cast<KeyItemView*>(child)->key_code() > key_code;
        });

    AddChildViewAt(CreateKeyItemView(key_code),
                   std::distance(children().begin(), iter));
  }

  int current_modifiers_ = 0;
};

BEGIN_METADATA(ModifiersContainerView)
END_METADATA

// -----------------------------------------------------------------------------
// KeyComboView:

KeyComboView::KeyComboView() {
  SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
      kBetweenKeyItemSpace));
}

KeyComboView::~KeyComboView() = default;

void KeyComboView::RefreshView(int modifiers,
                               ui::KeyboardCode last_non_modifier_key) {
  if (modifiers_ != modifiers) {
    modifiers_ = modifiers;
    if (!modifiers_container_view_) {
      modifiers_container_view_ = AddChildViewAt(
          std::make_unique<ModifiersContainerView>(), /*index=*/0);
    }

    modifiers_container_view_->RebuildModifiersContainerView(modifiers_);
  }

  if (last_non_modifier_key != last_non_modifier_key_) {
    last_non_modifier_key_ = last_non_modifier_key;
    if (non_modifier_view_) {
      RemoveChildViewT(non_modifier_view_.get());
      non_modifier_view_ = nullptr;
    }

    if (last_non_modifier_key != ui::VKEY_UNKNOWN) {
      non_modifier_view_ =
          AddChildView(CreateKeyItemView(last_non_modifier_key_));

      // Ensure to trigger a relayout so that the newly added
      // `non_modifier_view_` will be visible.
      InvalidateLayout();
    }
  }
}

std::vector<ui::KeyboardCode> KeyComboView::GetModifierKeycodeVector() const {
  std::vector<ui::KeyboardCode> key_codes;
  base::ranges::for_each(
      modifiers_container_view_->children(), [&](views::View* view) {
        key_codes.push_back(static_cast<KeyItemView*>(view)->key_code());
      });
  return key_codes;
}

BEGIN_METADATA(KeyComboView)
END_METADATA

}  // namespace ash