blob: a61f9061fd909a716b4d07c77451c6bee06bbbb2 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/base/accelerators/command.h"
#include <string>
#include <utility>
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "build/android_buildflags.h"
#include "build/build_config.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/accelerators/command_constants.h"
#include "ui/base/accelerators/media_keys_listener.h"
#include "ui/events/keycodes/keyboard_codes.h"
namespace ui {
namespace {
// Maximum number of tokens a shortcut can have if it allows the
// Ctrl+Alt shortcut combination.
#if BUILDFLAG(IS_CHROMEOS)
// ChromeOS supports an additional modifier 'Search', which can result in longer
// sequences.
static const int kMaxTokenSize = 5;
#else
static const int kMaxTokenSize = 4;
#endif // BUILDFLAG(IS_CHROMEOS)
bool DoesRequireModifier(ui::Accelerator accelerator) {
const KeyboardCode key_code = accelerator.key_code();
return key_code != ui::VKEY_MEDIA_NEXT_TRACK &&
key_code != ui::VKEY_MEDIA_PLAY_PAUSE &&
key_code != ui::VKEY_MEDIA_PREV_TRACK &&
key_code != ui::VKEY_MEDIA_STOP;
}
bool HasAnyModifierKeys(ui::Accelerator accelerator) {
return ui::Accelerator::MaskOutKeyEventFlags(accelerator.modifiers()) != 0;
}
bool HasValidModifierCombination(ui::Accelerator accelerator,
bool allow_ctrl_alt) {
// Must have a modifier
if (DoesRequireModifier(accelerator) && !HasAnyModifierKeys(accelerator)) {
return false;
}
// Usually Ctrl+Alt/Cmd+Option key combinations are not supported. See this
// article: https://devblogs.microsoft.com/oldnewthing/20040329-00/?p=40003
if (!allow_ctrl_alt && accelerator.IsAltDown() &&
(accelerator.IsCtrlDown() || accelerator.IsCmdDown())) {
return false;
}
if (accelerator.IsShiftDown()) {
return accelerator.IsCtrlDown() || accelerator.IsAltDown() ||
accelerator.IsCmdDown();
}
return true;
}
} // namespace
Command::Command(std::string_view command_name,
std::u16string_view description,
bool global)
: command_name_(command_name), description_(description), global_(global) {}
// static
std::string Command::CommandPlatform() {
#if BUILDFLAG(IS_WIN)
return ui::kKeybindingPlatformWin;
#elif BUILDFLAG(IS_MAC)
return ui::kKeybindingPlatformMac;
#elif BUILDFLAG(IS_CHROMEOS)
return ui::kKeybindingPlatformChromeOs;
#elif BUILDFLAG(IS_LINUX)
return ui::kKeybindingPlatformLinux;
#elif BUILDFLAG(IS_FUCHSIA)
// TODO(crbug.com/40220501): Change this once we decide what string should be
// used for Fuchsia.
return ui::kKeybindingPlatformLinux;
#elif BUILDFLAG(IS_DESKTOP_ANDROID)
// For now, we use linux keybindings on desktop android.
// TODO(https://crbug.com/356905053): Should this be ChromeOS keybindings?
return ui::kKeybindingPlatformLinux;
#else
return "";
#endif
}
// static
ui::Accelerator Command::StringToAccelerator(std::string_view accelerator) {
std::u16string error;
ui::Accelerator parsed =
ParseImpl(accelerator, CommandPlatform(), false, base::DoNothing(),
/*allow_ctrl_alt=*/true);
return parsed;
}
// static
std::string Command::AcceleratorToString(const ui::Accelerator& accelerator) {
if (!HasValidModifierCombination(accelerator, true)) {
return "";
}
std::string shortcut;
if (accelerator.IsCtrlDown()) {
shortcut += ui::kKeyCtrl;
shortcut += ui::kKeySeparator;
}
if (accelerator.IsAltDown()) {
shortcut += ui::kKeyAlt;
shortcut += ui::kKeySeparator;
}
if (accelerator.IsCmdDown()) {
#if BUILDFLAG(IS_CHROMEOS)
// Chrome OS treats the Search key like the Command key.
shortcut += ui::kKeySearch;
#else
shortcut += ui::kKeyCommand;
#endif
shortcut += ui::kKeySeparator;
}
if (accelerator.IsShiftDown()) {
shortcut += ui::kKeyShift;
shortcut += ui::kKeySeparator;
}
if (accelerator.key_code() >= ui::VKEY_0 &&
accelerator.key_code() <= ui::VKEY_9) {
shortcut += static_cast<char>('0' + (accelerator.key_code() - ui::VKEY_0));
} else if (accelerator.key_code() >= ui::VKEY_A &&
accelerator.key_code() <= ui::VKEY_Z) {
shortcut += static_cast<char>('A' + (accelerator.key_code() - ui::VKEY_A));
} else {
switch (accelerator.key_code()) {
case ui::VKEY_OEM_COMMA:
shortcut += ui::kKeyComma;
break;
case ui::VKEY_OEM_PERIOD:
shortcut += ui::kKeyPeriod;
break;
case ui::VKEY_UP:
shortcut += ui::kKeyUp;
break;
case ui::VKEY_DOWN:
shortcut += ui::kKeyDown;
break;
case ui::VKEY_LEFT:
shortcut += ui::kKeyLeft;
break;
case ui::VKEY_RIGHT:
shortcut += ui::kKeyRight;
break;
case ui::VKEY_INSERT:
shortcut += ui::kKeyIns;
break;
case ui::VKEY_DELETE:
shortcut += ui::kKeyDel;
break;
case ui::VKEY_HOME:
shortcut += ui::kKeyHome;
break;
case ui::VKEY_END:
shortcut += ui::kKeyEnd;
break;
case ui::VKEY_PRIOR:
shortcut += ui::kKeyPgUp;
break;
case ui::VKEY_NEXT:
shortcut += ui::kKeyPgDwn;
break;
case ui::VKEY_SPACE:
shortcut += ui::kKeySpace;
break;
case ui::VKEY_TAB:
shortcut += ui::kKeyTab;
break;
case ui::VKEY_MEDIA_NEXT_TRACK:
shortcut += ui::kKeyMediaNextTrack;
break;
case ui::VKEY_MEDIA_PLAY_PAUSE:
shortcut += ui::kKeyMediaPlayPause;
break;
case ui::VKEY_MEDIA_PREV_TRACK:
shortcut += ui::kKeyMediaPrevTrack;
break;
case ui::VKEY_MEDIA_STOP:
shortcut += ui::kKeyMediaStop;
break;
default:
return "";
}
}
return shortcut;
}
ui::Accelerator Command::ParseImpl(std::string_view accelerator,
std::string_view platform_key,
bool should_parse_media_keys,
AcceleratorParseErrorCallback error_callback,
bool allow_ctrl_alt) {
if (platform_key != ui::kKeybindingPlatformWin &&
platform_key != ui::kKeybindingPlatformMac &&
platform_key != ui::kKeybindingPlatformChromeOs &&
platform_key != ui::kKeybindingPlatformLinux &&
platform_key != ui::kKeybindingPlatformDefault) {
std::move(error_callback)
.Run(ui::AcceleratorParseError::kUnsupportedPlatform);
return ui::Accelerator();
}
// The max token size is reduced by one if the Ctrl+Alt shortcut combination
// is not allowed.
const size_t max_token_size =
allow_ctrl_alt ? kMaxTokenSize : kMaxTokenSize - 1;
std::vector<std::string> tokens = base::SplitString(
accelerator, "+", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
if (tokens.empty() || tokens.size() > max_token_size) {
std::move(error_callback).Run(ui::AcceleratorParseError::kMalformedInput);
return ui::Accelerator();
}
// Now, parse it into an accelerator.
int modifiers = ui::EF_NONE;
ui::KeyboardCode key = ui::VKEY_UNKNOWN;
for (const std::string& token : tokens) {
if (token == ui::kKeyCtrl) {
modifiers |= ui::EF_CONTROL_DOWN;
} else if (token == ui::kKeyCommand) {
if (platform_key == ui::kKeybindingPlatformMac) {
// Either the developer specified Command+foo in the manifest for Mac or
// they specified Ctrl and it got normalized to Command (to get Ctrl on
// Mac the developer has to specify MacCtrl). Therefore we treat this
// as Command.
modifiers |= ui::EF_COMMAND_DOWN;
#if BUILDFLAG(IS_MAC)
} else if (platform_key == ui::kKeybindingPlatformDefault) {
// If we see "Command+foo" in the Default section it can mean two
// things, depending on the platform:
// The developer specified "Ctrl+foo" for Default and it got normalized
// on Mac to "Command+foo". This is fine. Treat it as Command.
modifiers |= ui::EF_COMMAND_DOWN;
#endif
} else {
// No other platform supports Command.
key = ui::VKEY_UNKNOWN;
break;
}
} else if (token == ui::kKeySearch) {
// Search is a special modifier only on ChromeOS and maps to 'Command'.
if (platform_key == ui::kKeybindingPlatformChromeOs) {
modifiers |= ui::EF_COMMAND_DOWN;
} else {
// No other platform supports Search.
key = ui::VKEY_UNKNOWN;
break;
}
} else if (token == ui::kKeyAlt) {
modifiers |= ui::EF_ALT_DOWN;
} else if (token == ui::kKeyShift) {
modifiers |= ui::EF_SHIFT_DOWN;
} else if (token.size() == 1 || // A-Z, 0-9.
token == ui::kKeyComma || token == ui::kKeyPeriod ||
token == ui::kKeyUp || token == ui::kKeyDown ||
token == ui::kKeyLeft || token == ui::kKeyRight ||
token == ui::kKeyIns || token == ui::kKeyDel ||
token == ui::kKeyHome || token == ui::kKeyEnd ||
token == ui::kKeyPgUp || token == ui::kKeyPgDwn ||
token == ui::kKeySpace || token == ui::kKeyTab ||
token == ui::kKeyMediaNextTrack ||
token == ui::kKeyMediaPlayPause ||
token == ui::kKeyMediaPrevTrack || token == ui::kKeyMediaStop) {
if (key != ui::VKEY_UNKNOWN) {
// Multiple key assignments.
key = ui::VKEY_UNKNOWN;
break;
}
if (token == ui::kKeyComma) {
key = ui::VKEY_OEM_COMMA;
} else if (token == ui::kKeyPeriod) {
key = ui::VKEY_OEM_PERIOD;
} else if (token == ui::kKeyUp) {
key = ui::VKEY_UP;
} else if (token == ui::kKeyDown) {
key = ui::VKEY_DOWN;
} else if (token == ui::kKeyLeft) {
key = ui::VKEY_LEFT;
} else if (token == ui::kKeyRight) {
key = ui::VKEY_RIGHT;
} else if (token == ui::kKeyIns) {
key = ui::VKEY_INSERT;
} else if (token == ui::kKeyDel) {
key = ui::VKEY_DELETE;
} else if (token == ui::kKeyHome) {
key = ui::VKEY_HOME;
} else if (token == ui::kKeyEnd) {
key = ui::VKEY_END;
} else if (token == ui::kKeyPgUp) {
key = ui::VKEY_PRIOR;
} else if (token == ui::kKeyPgDwn) {
key = ui::VKEY_NEXT;
} else if (token == ui::kKeySpace) {
key = ui::VKEY_SPACE;
} else if (token == ui::kKeyTab) {
key = ui::VKEY_TAB;
} else if (token == ui::kKeyMediaNextTrack && should_parse_media_keys) {
key = ui::VKEY_MEDIA_NEXT_TRACK;
} else if (token == ui::kKeyMediaPlayPause && should_parse_media_keys) {
key = ui::VKEY_MEDIA_PLAY_PAUSE;
} else if (token == ui::kKeyMediaPrevTrack && should_parse_media_keys) {
key = ui::VKEY_MEDIA_PREV_TRACK;
} else if (token == ui::kKeyMediaStop && should_parse_media_keys) {
key = ui::VKEY_MEDIA_STOP;
} else if (token.size() == 1 && base::IsAsciiUpper(token[0])) {
key = static_cast<ui::KeyboardCode>(ui::VKEY_A + (token[0] - 'A'));
} else if (token.size() == 1 && base::IsAsciiDigit(token[0])) {
key = static_cast<ui::KeyboardCode>(ui::VKEY_0 + (token[0] - '0'));
} else {
key = ui::VKEY_UNKNOWN;
break;
}
} else {
std::move(error_callback).Run(ui::AcceleratorParseError::kMalformedInput);
return ui::Accelerator();
}
}
const ui::Accelerator parsed_accelerator(key, modifiers);
if (key == ui::VKEY_UNKNOWN ||
!HasValidModifierCombination(parsed_accelerator, allow_ctrl_alt)) {
std::move(error_callback).Run(ui::AcceleratorParseError::kMalformedInput);
return ui::Accelerator();
}
if (ui::MediaKeysListener::IsMediaKeycode(key) &&
HasAnyModifierKeys(parsed_accelerator)) {
std::move(error_callback)
.Run(ui::AcceleratorParseError::kMediaKeyWithModifier);
return ui::Accelerator();
}
return parsed_accelerator;
}
} // namespace ui