cef/tests/cefclient/browser/views_menu_bar.cc

349 lines
10 KiB
C++
Raw Normal View History

// 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"
#include "include/cef_i18n_util.h"
#include "include/views/cef_box_layout.h"
#include "include/views/cef_window.h"
#include "tests/cefclient/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.
char16_t ToLower(char16_t 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'.
char16_t GetMnemonic(const std::u16string& title) {
size_t index = 0;
do {
index = title.find('&', index);
if (index != std::u16string::npos) {
2023-01-02 23:59:03 +01:00
if (index + 1 != title.size() && title[index + 1] != '&') {
return ToLower(title[index + 1]);
2023-01-02 23:59:03 +01:00
}
index++;
}
} while (index != std::u16string::npos);
return 0;
}
} // namespace
ViewsMenuBar::ViewsMenuBar(Delegate* delegate,
int menu_id_start,
bool use_bottom_controls)
: delegate_(delegate),
id_start_(menu_id_start),
use_bottom_controls_(use_bottom_controls),
id_next_(menu_id_start) {
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_++;
2023-01-02 23:59:03 +01:00
if (menu_id) {
*menu_id = new_menu_id;
2023-01-02 23:59:03 +01:00
}
// 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);
button->SetID(new_menu_id);
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.
char16_t mnemonic = GetMnemonic(label);
2023-01-02 23:59:03 +01:00
if (mnemonic != 0) {
mnemonics_.insert(std::make_pair(mnemonic, new_menu_id));
2023-01-02 23:59:03 +01:00
}
return model;
}
CefRefPtr<CefMenuModel> ViewsMenuBar::GetMenuModel(int menu_id) const {
2023-01-02 23:59:03 +01:00
if (HasMenuId(menu_id)) {
return models_[menu_id - id_start_];
2023-01-02 23:59:03 +01:00
}
return nullptr;
}
void ViewsMenuBar::SetMenuFocusable(bool focusable) {
2023-01-02 23:59:03 +01:00
if (!panel_) {
return;
2023-01-02 23:59:03 +01:00
}
2023-01-02 23:59:03 +01:00
for (int id = id_start_; id < id_next_; ++id) {
panel_->GetViewForID(id)->SetFocusable(focusable);
2023-01-02 23:59:03 +01:00
}
if (focusable) {
// Give focus to the first MenuButton.
panel_->GetViewForID(id_start_)->RequestFocus();
}
}
bool ViewsMenuBar::OnKeyEvent(const CefKeyEvent& event) {
2023-01-02 23:59:03 +01:00
if (!panel_) {
return false;
2023-01-02 23:59:03 +01:00
}
2023-01-02 23:59:03 +01:00
if (event.type != KEYEVENT_RAWKEYDOWN) {
return false;
2023-01-02 23:59:03 +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)) {
return false;
2023-01-02 23:59:03 +01:00
}
MnemonicMap::const_iterator it = mnemonics_.find(ToLower(event.character));
2023-01-02 23:59:03 +01:00
if (it == mnemonics_.end()) {
return false;
2023-01-02 23:59:03 +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;
}
void ViewsMenuBar::Reset() {
panel_ = nullptr;
models_.clear();
mnemonics_.clear();
id_next_ = id_start_;
}
void ViewsMenuBar::OnMenuButtonPressed(
CefRefPtr<CefMenuButton> menu_button,
const CefPoint& screen_point,
CefRefPtr<CefMenuButtonPressedLock> button_pressed_lock) {
CefRefPtr<CefMenuModel> menu_model = GetMenuModel(menu_button->GetID());
const auto button_bounds = menu_button->GetBoundsInScreen();
// Adjust menu position to align with the button.
CefPoint point = screen_point;
if (CefIsRTL()) {
point.x += button_bounds.width - 4;
} else {
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.
const int menu_height =
static_cast<int>(menu_model->GetCount()) * button_bounds.height;
if (menu_height > available_height) {
// The menu will go upwards, so place it above the button.
point.y -= button_bounds.height - 8;
}
}
// 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)) {
return;
2023-01-02 23:59:03 +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)) {
return;
2023-01-02 23:59:03 +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)) {
return;
2023-01-02 23:59:03 +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) {
continue;
2023-01-02 23:59:03 +01:00
}
CefRefPtr<CefView> button = panel_->GetViewForID(id);
CefRect button_bounds = button->GetBounds();
// Adjust for window coordinates.
button_bounds.y += panel_bounds.y;
if (CefIsRTL()) {
// Adjust for right-to-left button layout.
button_bounds.x =
panel_bounds.width - button_bounds.x - button_bounds.width;
}
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_) {
last_nav_with_keyboard_ = false;
2023-01-02 23:59:03 +01:00
}
}
void ViewsMenuBar::OnThemeChanged(CefRefPtr<CefView> view) {
// Apply colors when the theme changes.
views: Add support for OS and Chrome themes (fixes #3610, fixes #3671) Controls now respect OS and Chrome themes by default for both Alloy and Chrome runtimes. Chrome themes (mode and colors) can be configured using the new CefRequestContext::SetChromeColorScheme method. Individual theme colors can be overridden using the new CefWindowDelegate:: OnThemeColorsChanged and CefWindow::SetThemeColor methods. The `--force-light-mode` and `--force-dark-mode` command-line flags are now respected on all platforms as an override for the OS theme. The current Chrome theme, if any, will take precedence over the OS theme when determining light/dark status. On Windows and MacOS the titlebar color will also be updated to match the light/dark theme. Testable as follows: - Run: `cefclient --enable-chrome-runtime` OR `cefclient --use-views --persist-user-preferences --cache-path=...` - App launches with default OS light/dark theme colors. - Change OS dark/light theme under system settings. Notice that theme colors change as expected. - Right click, select items from the new Theme sub-menu. Notice that theme colors behave as expected. - Exit and relaunch the app. Notice that the last-used theme colors are applied on app restart. - Add `--background-color=green` to above command-line. - Perform the same actions as above. Notice that all controls start and remain green throughout (except some icons with Chrome runtime). - Add `--force-light-mode` or `--force-dark-mode` to above command-line. - Perform the same actions as above. Notice that OS dark/light theme changes are ignored, but Chrome theme changes work as expected.
2024-03-29 17:48:33 +01:00
views_style::OnThemeChanged(view);
}
void ViewsMenuBar::EnsureMenuPanel() {
2023-01-02 23:59:03 +01:00
if (panel_) {
return;
2023-01-02 23:59:03 +01:00
}
panel_ = CefPanel::CreatePanel(this);
// 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) {
return id;
2023-01-02 23:59:03 +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) {
next_menu_index += menu_count;
2023-01-02 23:59:03 +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()) {
menu_button->RequestFocus();
2023-01-02 23:59:03 +01:00
}
menu_button->TriggerMenu();
}
} // namespace client