views_menu_bar.cc 8.67 KB
// Copyright (c) 2017 The Chromium Embedded Framework 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 "../browser/views_menu_bar.h"

#include "include/views/cef_box_layout.h"
#include "include/views/cef_window.h"
#include "../browser/views_style.h"

namespace client {

namespace {

const int kMenuBarGroupId = 100;

// Convert |c| to lowercase using the current ICU locale.
// TODO(jshin): What about Turkish locale? See http://crbug.com/81719.
// If the mnemonic is capital I and the UI language is Turkish, lowercasing it
// results in 'small dotless i', which is different from a 'dotted i'. Similar
// issues may exist for az and lt locales.
base::char16 ToLower(base::char16 c) {
  CefStringUTF16 str16;
  cef_string_utf16_to_lower(&c, 1, str16.GetWritableStruct());
  return str16.length() > 0 ? str16.c_str()[0] : 0;
}

// Extract the mnemonic character from |title|. For example, if |title| is
// "&Test" then the mnemonic character is 'T'.
base::char16 GetMnemonic(const base::string16& title) {
  size_t index = 0;
  do {
    index = title.find('&', index);
    if (index != base::string16::npos) {
      if (index + 1 != title.size() && title[index + 1] != '&')
        return ToLower(title[index + 1]);
      index++;
    }
  } while (index != base::string16::npos);
  return 0;
}

}  // namespace

ViewsMenuBar::ViewsMenuBar(Delegate* delegate, int menu_id_start)
    : delegate_(delegate),
      id_start_(menu_id_start),
      id_next_(menu_id_start),
      last_nav_with_keyboard_(false) {
  DCHECK(delegate_);
  DCHECK_GT(id_start_, 0);
}

bool ViewsMenuBar::HasMenuId(int menu_id) const {
  return menu_id >= id_start_ && menu_id < id_next_;
}

CefRefPtr<CefPanel> ViewsMenuBar::GetMenuPanel() {
  EnsureMenuPanel();
  return panel_;
}

CefRefPtr<CefMenuModel> ViewsMenuBar::CreateMenuModel(const CefString& label,
                                                      int* menu_id) {
  EnsureMenuPanel();

  // Assign the new menu ID.
  const int new_menu_id = id_next_++;
  if (menu_id)
    *menu_id = new_menu_id;

  // Create the new MenuModel.
  CefRefPtr<CefMenuModel> model = CefMenuModel::CreateMenuModel(this);
  views_style::ApplyTo(model);
  models_.push_back(model);

  // Create the new MenuButton.
  CefRefPtr<CefMenuButton> button =
      CefMenuButton::CreateMenuButton(this, label, false, false);
  button->SetID(new_menu_id);
  views_style::ApplyTo(button.get());
  button->SetInkDropEnabled(true);

  // Assign a group ID to allow focus traversal between MenuButtons using the
  // arrow keys when the menu is not displayed.
  button->SetGroupID(kMenuBarGroupId);

  // Add the new MenuButton to the Planel.
  panel_->AddChildView(button);

  // Extract the mnemonic that triggers the menu, if any.
  base::char16 mnemonic = GetMnemonic(label);
  if (mnemonic != 0)
    mnemonics_.insert(std::make_pair(mnemonic, new_menu_id));

  return model;
}

CefRefPtr<CefMenuModel> ViewsMenuBar::GetMenuModel(int menu_id) const {
  if (HasMenuId(menu_id))
    return models_[menu_id - id_start_];
  return NULL;
}

void ViewsMenuBar::SetMenuFocusable(bool focusable) {
  if (!panel_)
    return;

  for (int id = id_start_; id < id_next_; ++id)
    panel_->GetViewForID(id)->SetFocusable(focusable);

  if (focusable) {
    // Give focus to the first MenuButton.
    panel_->GetViewForID(id_start_)->RequestFocus();
  }
}

bool ViewsMenuBar::OnKeyEvent(const CefKeyEvent& event) {
  if (!panel_)
    return false;

  if (event.type != KEYEVENT_RAWKEYDOWN)
    return false;

  // Do not check mnemonics if the Alt or Ctrl modifiers are pressed. For
  // example Ctrl+<T> is an accelerator, but <T> only is a mnemonic.
  if (event.modifiers & (EVENTFLAG_ALT_DOWN | EVENTFLAG_CONTROL_DOWN))
    return false;

  MnemonicMap::const_iterator it = mnemonics_.find(ToLower(event.character));
  if (it == mnemonics_.end())
    return false;

  // Set status indicating that we navigated using the keyboard.
  last_nav_with_keyboard_ = true;

  // Show the selected menu.
  TriggerMenuButton(panel_->GetViewForID(it->second));

  return true;
}

void ViewsMenuBar::Reset() {
  panel_ = NULL;
  models_.clear();
  mnemonics_.clear();
  id_next_ = id_start_;
}

void ViewsMenuBar::OnMenuButtonPressed(CefRefPtr<CefMenuButton> menu_button,
                                       const CefPoint& screen_point) {
  CefRefPtr<CefMenuModel> menu_model = GetMenuModel(menu_button->GetID());

  // Adjust menu position left by button width.
  CefPoint point = screen_point;
  point.x -= menu_button->GetBounds().width - 4;

  // Keep track of the current |last_nav_with_keyboard_| status and restore it
  // after displaying the new menu.
  bool cur_last_nav_with_keyboard = last_nav_with_keyboard_;

  // May result in the previous menu being closed, in which case MenuClosed will
  // be called before the new menu is displayed.
  menu_button->ShowMenu(menu_model, point, CEF_MENU_ANCHOR_TOPLEFT);

  last_nav_with_keyboard_ = cur_last_nav_with_keyboard;
}

void ViewsMenuBar::ExecuteCommand(CefRefPtr<CefMenuModel> menu_model,
                                  int command_id,
                                  cef_event_flags_t event_flags) {
  delegate_->MenuBarExecuteCommand(menu_model, command_id, event_flags);
}

void ViewsMenuBar::MouseOutsideMenu(CefRefPtr<CefMenuModel> menu_model,
                                    const CefPoint& screen_point) {
  DCHECK(panel_);

  // Retrieve the Window hosting the Panel.
  CefRefPtr<CefWindow> window = panel_->GetWindow();
  DCHECK(window);

  // Convert the point from screen to window coordinates.
  CefPoint window_point = screen_point;
  if (!window->ConvertPointFromScreen(window_point))
    return;

  CefRect panel_bounds = panel_->GetBounds();

  if (last_nav_with_keyboard_) {
    // The user navigated last using the keyboard. Don't change menus using
    // mouse movements until the mouse exits and re-enters the Panel.
    if (panel_bounds.Contains(window_point))
      return;
    last_nav_with_keyboard_ = false;
  }

  // Check that the point is inside the Panel.
  if (!panel_bounds.Contains(window_point))
    return;

  const int active_menu_id = GetActiveMenuId();

  // Determine which MenuButton is under the specified point.
  for (int id = id_start_; id < id_next_; ++id) {
    // Skip the currently active MenuButton.
    if (id == active_menu_id)
      continue;

    CefRefPtr<CefView> button = panel_->GetViewForID(id);
    CefRect button_bounds = button->GetBounds();
    if (button_bounds.Contains(window_point)) {
      // Trigger the hovered MenuButton.
      TriggerMenuButton(button);
      break;
    }
  }
}

void ViewsMenuBar::UnhandledOpenSubmenu(CefRefPtr<CefMenuModel> menu_model,
                                        bool is_rtl) {
  TriggerNextMenu(is_rtl ? 1 : -1);
}

void ViewsMenuBar::UnhandledCloseSubmenu(CefRefPtr<CefMenuModel> menu_model,
                                         bool is_rtl) {
  TriggerNextMenu(is_rtl ? -1 : 1);
}

void ViewsMenuBar::MenuClosed(CefRefPtr<CefMenuModel> menu_model) {
  // Reset |last_nav_with_keyboard_| status whenever the main menu closes.
  if (!menu_model->IsSubMenu() && last_nav_with_keyboard_)
    last_nav_with_keyboard_ = false;
}

void ViewsMenuBar::EnsureMenuPanel() {
  if (panel_)
    return;

  panel_ = CefPanel::CreatePanel(NULL);
  views_style::ApplyTo(panel_);

  // Use a horizontal box layout.
  CefBoxLayoutSettings top_panel_layout_settings;
  top_panel_layout_settings.horizontal = true;
  panel_->SetToBoxLayout(top_panel_layout_settings);
}

int ViewsMenuBar::GetActiveMenuId() {
  DCHECK(panel_);

  for (int id = id_start_; id < id_next_; ++id) {
    CefRefPtr<CefButton> button = panel_->GetViewForID(id)->AsButton();
    if (button->GetState() == CEF_BUTTON_STATE_PRESSED)
      return id;
  }

  return -1;
}

void ViewsMenuBar::TriggerNextMenu(int offset) {
  DCHECK(panel_);

  const int active_menu_id = GetActiveMenuId();
  const int menu_count = id_next_ - id_start_;
  const int active_menu_index = active_menu_id - id_start_;

  // Compute the modulus to avoid negative values.
  int next_menu_index = (active_menu_index + offset) % menu_count;
  if (next_menu_index < 0)
    next_menu_index += menu_count;

  // Cancel the existing menu. MenuClosed may be called.
  panel_->GetWindow()->CancelMenu();

  // Set status indicating that we navigated using the keyboard.
  last_nav_with_keyboard_ = true;

  // Show the new menu.
  TriggerMenuButton(panel_->GetViewForID(id_start_ + next_menu_index));
}

void ViewsMenuBar::TriggerMenuButton(CefRefPtr<CefView> button) {
  CefRefPtr<CefMenuButton> menu_button =
      button->AsButton()->AsLabelButton()->AsMenuButton();
  if (menu_button->IsFocusable())
    menu_button->RequestFocus();
  menu_button->TriggerMenu();
}

}  // namespace client