2017-02-22 19:05:27 +01:00
|
|
|
// 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 "tests/cefclient/browser/views_menu_bar.h"
|
|
|
|
|
2021-08-28 03:55:15 +02:00
|
|
|
#include "include/cef_i18n_util.h"
|
2017-02-22 19:05:27 +01:00
|
|
|
#include "include/views/cef_box_layout.h"
|
|
|
|
#include "include/views/cef_window.h"
|
2017-02-25 06:03:31 +01:00
|
|
|
#include "tests/cefclient/browser/views_style.h"
|
2017-02-22 19:05:27 +01:00
|
|
|
|
|
|
|
namespace client {
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
const int kMenuBarGroupId = 100;
|
|
|
|
|
2017-02-23 21:24:45 +01:00
|
|
|
// 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.
|
2023-06-01 16:06:15 +02:00
|
|
|
char16_t ToLower(char16_t c) {
|
2017-02-23 21:24:45 +01:00
|
|
|
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'.
|
2023-06-01 16:06:15 +02:00
|
|
|
char16_t GetMnemonic(const std::u16string& title) {
|
2017-02-23 21:24:45 +01:00
|
|
|
size_t index = 0;
|
|
|
|
do {
|
|
|
|
index = title.find('&', index);
|
2021-04-21 00:52:34 +02:00
|
|
|
if (index != std::u16string::npos) {
|
2023-01-02 23:59:03 +01:00
|
|
|
if (index + 1 != title.size() && title[index + 1] != '&') {
|
2017-02-23 21:24:45 +01:00
|
|
|
return ToLower(title[index + 1]);
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-23 21:24:45 +01:00
|
|
|
index++;
|
|
|
|
}
|
2021-04-21 00:52:34 +02:00
|
|
|
} while (index != std::u16string::npos);
|
2017-02-23 21:24:45 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2017-02-22 19:05:27 +01:00
|
|
|
} // namespace
|
|
|
|
|
2023-06-30 14:17:02 +02:00
|
|
|
ViewsMenuBar::ViewsMenuBar(Delegate* delegate,
|
|
|
|
int menu_id_start,
|
|
|
|
bool use_bottom_controls)
|
2017-05-17 11:29:28 +02:00
|
|
|
: delegate_(delegate),
|
|
|
|
id_start_(menu_id_start),
|
2023-06-30 14:17:02 +02:00
|
|
|
use_bottom_controls_(use_bottom_controls),
|
2024-01-20 03:22:56 +01:00
|
|
|
id_next_(menu_id_start) {
|
2017-02-22 19:05:27 +01:00
|
|
|
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_;
|
|
|
|
}
|
|
|
|
|
2017-02-23 21:24:45 +01:00
|
|
|
CefRefPtr<CefMenuModel> ViewsMenuBar::CreateMenuModel(const CefString& label,
|
2017-02-22 19:05:27 +01:00
|
|
|
int* menu_id) {
|
|
|
|
EnsureMenuPanel();
|
|
|
|
|
|
|
|
// Assign the new menu ID.
|
|
|
|
const int new_menu_id = id_next_++;
|
2023-01-02 23:59:03 +01:00
|
|
|
if (menu_id) {
|
2017-02-22 19:05:27 +01:00
|
|
|
*menu_id = new_menu_id;
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-22 19:05:27 +01:00
|
|
|
|
|
|
|
// Create the new MenuModel.
|
|
|
|
CefRefPtr<CefMenuModel> model = CefMenuModel::CreateMenuModel(this);
|
2017-02-25 06:03:31 +01:00
|
|
|
views_style::ApplyTo(model);
|
2017-02-22 19:05:27 +01:00
|
|
|
models_.push_back(model);
|
|
|
|
|
|
|
|
// Create the new MenuButton.
|
|
|
|
CefRefPtr<CefMenuButton> button =
|
2019-07-16 19:59:21 +02:00
|
|
|
CefMenuButton::CreateMenuButton(this, label);
|
2017-02-22 19:05:27 +01:00
|
|
|
button->SetID(new_menu_id);
|
2017-02-24 19:02:07 +01:00
|
|
|
button->SetInkDropEnabled(true);
|
2017-02-22 19:05:27 +01:00
|
|
|
|
|
|
|
// 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);
|
2017-02-23 21:24:45 +01:00
|
|
|
|
|
|
|
// Extract the mnemonic that triggers the menu, if any.
|
2023-06-01 16:06:15 +02:00
|
|
|
char16_t mnemonic = GetMnemonic(label);
|
2023-01-02 23:59:03 +01:00
|
|
|
if (mnemonic != 0) {
|
2017-02-23 21:24:45 +01:00
|
|
|
mnemonics_.insert(std::make_pair(mnemonic, new_menu_id));
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-05-17 11:29:28 +02:00
|
|
|
|
2017-02-22 19:05:27 +01:00
|
|
|
return model;
|
|
|
|
}
|
|
|
|
|
|
|
|
CefRefPtr<CefMenuModel> ViewsMenuBar::GetMenuModel(int menu_id) const {
|
2023-01-02 23:59:03 +01:00
|
|
|
if (HasMenuId(menu_id)) {
|
2017-02-22 19:05:27 +01:00
|
|
|
return models_[menu_id - id_start_];
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2020-01-15 15:28:12 +01:00
|
|
|
return nullptr;
|
2017-02-22 19:05:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void ViewsMenuBar::SetMenuFocusable(bool focusable) {
|
2023-01-02 23:59:03 +01:00
|
|
|
if (!panel_) {
|
2017-02-22 19:05:27 +01:00
|
|
|
return;
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-22 19:05:27 +01:00
|
|
|
|
2023-01-02 23:59:03 +01:00
|
|
|
for (int id = id_start_; id < id_next_; ++id) {
|
2017-02-22 19:05:27 +01:00
|
|
|
panel_->GetViewForID(id)->SetFocusable(focusable);
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-22 19:05:27 +01:00
|
|
|
|
|
|
|
if (focusable) {
|
|
|
|
// Give focus to the first MenuButton.
|
|
|
|
panel_->GetViewForID(id_start_)->RequestFocus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-23 21:24:45 +01:00
|
|
|
bool ViewsMenuBar::OnKeyEvent(const CefKeyEvent& event) {
|
2023-01-02 23:59:03 +01:00
|
|
|
if (!panel_) {
|
2017-02-23 21:24:45 +01:00
|
|
|
return false;
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-23 21:24:45 +01:00
|
|
|
|
2023-01-02 23:59:03 +01:00
|
|
|
if (event.type != KEYEVENT_RAWKEYDOWN) {
|
2017-02-23 21:24:45 +01:00
|
|
|
return false;
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-23 21:24:45 +01:00
|
|
|
|
|
|
|
// 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.
|
2023-01-02 23:59:03 +01:00
|
|
|
if (event.modifiers & (EVENTFLAG_ALT_DOWN | EVENTFLAG_CONTROL_DOWN)) {
|
2017-02-23 21:24:45 +01:00
|
|
|
return false;
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-23 21:24:45 +01:00
|
|
|
|
|
|
|
MnemonicMap::const_iterator it = mnemonics_.find(ToLower(event.character));
|
2023-01-02 23:59:03 +01:00
|
|
|
if (it == mnemonics_.end()) {
|
2017-02-23 21:24:45 +01:00
|
|
|
return false;
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-23 21:24:45 +01:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2017-02-22 19:05:27 +01:00
|
|
|
void ViewsMenuBar::Reset() {
|
2020-01-15 15:28:12 +01:00
|
|
|
panel_ = nullptr;
|
2017-02-22 19:05:27 +01:00
|
|
|
models_.clear();
|
2017-02-23 21:24:45 +01:00
|
|
|
mnemonics_.clear();
|
2017-02-22 19:05:27 +01:00
|
|
|
id_next_ = id_start_;
|
|
|
|
}
|
|
|
|
|
2017-08-04 00:55:19 +02:00
|
|
|
void ViewsMenuBar::OnMenuButtonPressed(
|
|
|
|
CefRefPtr<CefMenuButton> menu_button,
|
|
|
|
const CefPoint& screen_point,
|
|
|
|
CefRefPtr<CefMenuButtonPressedLock> button_pressed_lock) {
|
2017-02-22 19:05:27 +01:00
|
|
|
CefRefPtr<CefMenuModel> menu_model = GetMenuModel(menu_button->GetID());
|
|
|
|
|
2023-06-30 14:17:02 +02:00
|
|
|
const auto button_bounds = menu_button->GetBoundsInScreen();
|
|
|
|
|
2021-08-28 03:55:15 +02:00
|
|
|
// Adjust menu position to align with the button.
|
2017-02-22 19:05:27 +01:00
|
|
|
CefPoint point = screen_point;
|
2021-08-28 03:55:15 +02:00
|
|
|
if (CefIsRTL()) {
|
2023-06-30 14:17:02 +02:00
|
|
|
point.x += button_bounds.width - 4;
|
2021-08-28 03:55:15 +02:00
|
|
|
} else {
|
2023-06-30 14:17:02 +02:00
|
|
|
point.x -= button_bounds.width - 4;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (use_bottom_controls_) {
|
|
|
|
const auto display_bounds =
|
|
|
|
menu_button->GetWindow()->GetDisplay()->GetWorkArea();
|
|
|
|
const int available_height = display_bounds.y + display_bounds.height -
|
|
|
|
button_bounds.y - button_bounds.height;
|
|
|
|
|
|
|
|
// Approximation of the menu height.
|
2023-07-06 12:19:51 +02:00
|
|
|
const int menu_height =
|
|
|
|
static_cast<int>(menu_model->GetCount()) * button_bounds.height;
|
2023-06-30 14:17:02 +02:00
|
|
|
if (menu_height > available_height) {
|
|
|
|
// The menu will go upwards, so place it above the button.
|
|
|
|
point.y -= button_bounds.height - 8;
|
|
|
|
}
|
2021-08-28 03:55:15 +02:00
|
|
|
}
|
2017-02-22 19:05:27 +01:00
|
|
|
|
|
|
|
// 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;
|
2023-01-02 23:59:03 +01:00
|
|
|
if (!window->ConvertPointFromScreen(window_point)) {
|
2017-02-22 19:05:27 +01:00
|
|
|
return;
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-22 19:05:27 +01:00
|
|
|
|
|
|
|
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.
|
2023-01-02 23:59:03 +01:00
|
|
|
if (panel_bounds.Contains(window_point)) {
|
2017-02-22 19:05:27 +01:00
|
|
|
return;
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-22 19:05:27 +01:00
|
|
|
last_nav_with_keyboard_ = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check that the point is inside the Panel.
|
2023-01-02 23:59:03 +01:00
|
|
|
if (!panel_bounds.Contains(window_point)) {
|
2017-02-22 19:05:27 +01:00
|
|
|
return;
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-22 19:05:27 +01:00
|
|
|
|
|
|
|
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.
|
2023-01-02 23:59:03 +01:00
|
|
|
if (id == active_menu_id) {
|
2017-02-22 19:05:27 +01:00
|
|
|
continue;
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-22 19:05:27 +01:00
|
|
|
|
|
|
|
CefRefPtr<CefView> button = panel_->GetViewForID(id);
|
|
|
|
CefRect button_bounds = button->GetBounds();
|
2023-06-30 14:17:02 +02:00
|
|
|
|
|
|
|
// Adjust for window coordinates.
|
|
|
|
button_bounds.y += panel_bounds.y;
|
|
|
|
|
2021-08-28 03:55:15 +02:00
|
|
|
if (CefIsRTL()) {
|
|
|
|
// Adjust for right-to-left button layout.
|
|
|
|
button_bounds.x =
|
|
|
|
panel_bounds.width - button_bounds.x - button_bounds.width;
|
|
|
|
}
|
2017-02-22 19:05:27 +01:00
|
|
|
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.
|
2023-01-02 23:59:03 +01:00
|
|
|
if (!menu_model->IsSubMenu() && last_nav_with_keyboard_) {
|
2017-02-22 19:05:27 +01:00
|
|
|
last_nav_with_keyboard_ = false;
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-22 19:05:27 +01:00
|
|
|
}
|
|
|
|
|
2024-03-23 00:51:00 +01:00
|
|
|
void ViewsMenuBar::OnThemeChanged(CefRefPtr<CefView> view) {
|
|
|
|
// Apply colors when the theme changes.
|
|
|
|
views_style::ApplyTo(view);
|
|
|
|
}
|
|
|
|
|
2017-02-22 19:05:27 +01:00
|
|
|
void ViewsMenuBar::EnsureMenuPanel() {
|
2023-01-02 23:59:03 +01:00
|
|
|
if (panel_) {
|
2017-02-22 19:05:27 +01:00
|
|
|
return;
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-22 19:05:27 +01:00
|
|
|
|
2024-03-23 00:51:00 +01:00
|
|
|
panel_ = CefPanel::CreatePanel(this);
|
2017-02-22 19:05:27 +01:00
|
|
|
|
|
|
|
// 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();
|
2023-01-02 23:59:03 +01:00
|
|
|
if (button->GetState() == CEF_BUTTON_STATE_PRESSED) {
|
2017-02-22 19:05:27 +01:00
|
|
|
return id;
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-22 19:05:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2023-01-02 23:59:03 +01:00
|
|
|
if (next_menu_index < 0) {
|
2017-02-22 19:05:27 +01:00
|
|
|
next_menu_index += menu_count;
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-22 19:05:27 +01:00
|
|
|
|
|
|
|
// 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();
|
2023-01-02 23:59:03 +01:00
|
|
|
if (menu_button->IsFocusable()) {
|
2017-02-22 19:05:27 +01:00
|
|
|
menu_button->RequestFocus();
|
2023-01-02 23:59:03 +01:00
|
|
|
}
|
2017-02-22 19:05:27 +01:00
|
|
|
menu_button->TriggerMenu();
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace client
|