command.cc revision 116680a4aac90f2aa7413d9095a592090648e557
1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "chrome/common/extensions/command.h" 6 7#include "base/logging.h" 8#include "base/strings/string_number_conversions.h" 9#include "base/strings/string_split.h" 10#include "base/strings/string_util.h" 11#include "base/values.h" 12#include "extensions/common/error_utils.h" 13#include "extensions/common/extension.h" 14#include "extensions/common/manifest_constants.h" 15#include "grit/generated_resources.h" 16#include "ui/base/l10n/l10n_util.h" 17 18namespace extensions { 19 20namespace errors = manifest_errors; 21namespace keys = manifest_keys; 22namespace values = manifest_values; 23 24namespace { 25 26static const char kMissing[] = "Missing"; 27 28static const char kCommandKeyNotSupported[] = 29 "Command key is not supported. Note: Ctrl means Command on Mac"; 30 31bool IsNamedCommand(const std::string& command_name) { 32 return command_name != values::kPageActionCommandEvent && 33 command_name != values::kBrowserActionCommandEvent; 34} 35 36bool DoesRequireModifier(const std::string& accelerator) { 37 return accelerator != values::kKeyMediaNextTrack && 38 accelerator != values::kKeyMediaPlayPause && 39 accelerator != values::kKeyMediaPrevTrack && 40 accelerator != values::kKeyMediaStop; 41} 42 43ui::Accelerator ParseImpl(const std::string& accelerator, 44 const std::string& platform_key, 45 int index, 46 bool should_parse_media_keys, 47 base::string16* error) { 48 error->clear(); 49 if (platform_key != values::kKeybindingPlatformWin && 50 platform_key != values::kKeybindingPlatformMac && 51 platform_key != values::kKeybindingPlatformChromeOs && 52 platform_key != values::kKeybindingPlatformLinux && 53 platform_key != values::kKeybindingPlatformDefault) { 54 *error = ErrorUtils::FormatErrorMessageUTF16( 55 errors::kInvalidKeyBindingUnknownPlatform, 56 base::IntToString(index), 57 platform_key); 58 return ui::Accelerator(); 59 } 60 61 std::vector<std::string> tokens; 62 base::SplitString(accelerator, '+', &tokens); 63 if (tokens.size() == 0 || 64 (tokens.size() == 1 && DoesRequireModifier(accelerator)) || 65 tokens.size() > 3) { 66 *error = ErrorUtils::FormatErrorMessageUTF16( 67 errors::kInvalidKeyBinding, 68 base::IntToString(index), 69 platform_key, 70 accelerator); 71 return ui::Accelerator(); 72 } 73 74 // Now, parse it into an accelerator. 75 int modifiers = ui::EF_NONE; 76 ui::KeyboardCode key = ui::VKEY_UNKNOWN; 77 for (size_t i = 0; i < tokens.size(); i++) { 78 if (tokens[i] == values::kKeyCtrl) { 79 modifiers |= ui::EF_CONTROL_DOWN; 80 } else if (tokens[i] == values::kKeyCommand) { 81 if (platform_key == values::kKeybindingPlatformMac) { 82 // Either the developer specified Command+foo in the manifest for Mac or 83 // they specified Ctrl and it got normalized to Command (to get Ctrl on 84 // Mac the developer has to specify MacCtrl). Therefore we treat this 85 // as Command. 86 modifiers |= ui::EF_COMMAND_DOWN; 87#if defined(OS_MACOSX) 88 } else if (platform_key == values::kKeybindingPlatformDefault) { 89 // If we see "Command+foo" in the Default section it can mean two 90 // things, depending on the platform: 91 // The developer specified "Ctrl+foo" for Default and it got normalized 92 // on Mac to "Command+foo". This is fine. Treat it as Command. 93 modifiers |= ui::EF_COMMAND_DOWN; 94#endif 95 } else { 96 // No other platform supports Command. 97 key = ui::VKEY_UNKNOWN; 98 break; 99 } 100 } else if (tokens[i] == values::kKeyAlt) { 101 modifiers |= ui::EF_ALT_DOWN; 102 } else if (tokens[i] == values::kKeyShift) { 103 modifiers |= ui::EF_SHIFT_DOWN; 104 } else if (tokens[i].size() == 1 || // A-Z, 0-9. 105 tokens[i] == values::kKeyComma || 106 tokens[i] == values::kKeyPeriod || 107 tokens[i] == values::kKeyUp || 108 tokens[i] == values::kKeyDown || 109 tokens[i] == values::kKeyLeft || 110 tokens[i] == values::kKeyRight || 111 tokens[i] == values::kKeyIns || 112 tokens[i] == values::kKeyDel || 113 tokens[i] == values::kKeyHome || 114 tokens[i] == values::kKeyEnd || 115 tokens[i] == values::kKeyPgUp || 116 tokens[i] == values::kKeyPgDwn || 117 tokens[i] == values::kKeyTab || 118 tokens[i] == values::kKeyMediaNextTrack || 119 tokens[i] == values::kKeyMediaPlayPause || 120 tokens[i] == values::kKeyMediaPrevTrack || 121 tokens[i] == values::kKeyMediaStop) { 122 if (key != ui::VKEY_UNKNOWN) { 123 // Multiple key assignments. 124 key = ui::VKEY_UNKNOWN; 125 break; 126 } 127 128 if (tokens[i] == values::kKeyComma) { 129 key = ui::VKEY_OEM_COMMA; 130 } else if (tokens[i] == values::kKeyPeriod) { 131 key = ui::VKEY_OEM_PERIOD; 132 } else if (tokens[i] == values::kKeyUp) { 133 key = ui::VKEY_UP; 134 } else if (tokens[i] == values::kKeyDown) { 135 key = ui::VKEY_DOWN; 136 } else if (tokens[i] == values::kKeyLeft) { 137 key = ui::VKEY_LEFT; 138 } else if (tokens[i] == values::kKeyRight) { 139 key = ui::VKEY_RIGHT; 140 } else if (tokens[i] == values::kKeyIns) { 141 key = ui::VKEY_INSERT; 142 } else if (tokens[i] == values::kKeyDel) { 143 key = ui::VKEY_DELETE; 144 } else if (tokens[i] == values::kKeyHome) { 145 key = ui::VKEY_HOME; 146 } else if (tokens[i] == values::kKeyEnd) { 147 key = ui::VKEY_END; 148 } else if (tokens[i] == values::kKeyPgUp) { 149 key = ui::VKEY_PRIOR; 150 } else if (tokens[i] == values::kKeyPgDwn) { 151 key = ui::VKEY_NEXT; 152 } else if (tokens[i] == values::kKeyTab) { 153 key = ui::VKEY_TAB; 154 } else if (tokens[i] == values::kKeyMediaNextTrack && 155 should_parse_media_keys) { 156 key = ui::VKEY_MEDIA_NEXT_TRACK; 157 } else if (tokens[i] == values::kKeyMediaPlayPause && 158 should_parse_media_keys) { 159 key = ui::VKEY_MEDIA_PLAY_PAUSE; 160 } else if (tokens[i] == values::kKeyMediaPrevTrack && 161 should_parse_media_keys) { 162 key = ui::VKEY_MEDIA_PREV_TRACK; 163 } else if (tokens[i] == values::kKeyMediaStop && 164 should_parse_media_keys) { 165 key = ui::VKEY_MEDIA_STOP; 166 } else if (tokens[i].size() == 1 && 167 tokens[i][0] >= 'A' && tokens[i][0] <= 'Z') { 168 key = static_cast<ui::KeyboardCode>(ui::VKEY_A + (tokens[i][0] - 'A')); 169 } else if (tokens[i].size() == 1 && 170 tokens[i][0] >= '0' && tokens[i][0] <= '9') { 171 key = static_cast<ui::KeyboardCode>(ui::VKEY_0 + (tokens[i][0] - '0')); 172 } else { 173 key = ui::VKEY_UNKNOWN; 174 break; 175 } 176 } else { 177 *error = ErrorUtils::FormatErrorMessageUTF16( 178 errors::kInvalidKeyBinding, 179 base::IntToString(index), 180 platform_key, 181 accelerator); 182 return ui::Accelerator(); 183 } 184 } 185 186 bool command = (modifiers & ui::EF_COMMAND_DOWN) != 0; 187 bool ctrl = (modifiers & ui::EF_CONTROL_DOWN) != 0; 188 bool alt = (modifiers & ui::EF_ALT_DOWN) != 0; 189 bool shift = (modifiers & ui::EF_SHIFT_DOWN) != 0; 190 191 // We support Ctrl+foo, Alt+foo, Ctrl+Shift+foo, Alt+Shift+foo, but not 192 // Ctrl+Alt+foo and not Shift+foo either. For a more detailed reason why we 193 // don't support Ctrl+Alt+foo see this article: 194 // http://blogs.msdn.com/b/oldnewthing/archive/2004/03/29/101121.aspx. 195 // On Mac Command can also be used in combination with Shift or on its own, 196 // as a modifier. 197 if (key == ui::VKEY_UNKNOWN || (ctrl && alt) || (command && alt) || 198 (shift && !ctrl && !alt && !command)) { 199 *error = ErrorUtils::FormatErrorMessageUTF16( 200 errors::kInvalidKeyBinding, 201 base::IntToString(index), 202 platform_key, 203 accelerator); 204 return ui::Accelerator(); 205 } 206 207 if ((key == ui::VKEY_MEDIA_NEXT_TRACK || 208 key == ui::VKEY_MEDIA_PREV_TRACK || 209 key == ui::VKEY_MEDIA_PLAY_PAUSE || 210 key == ui::VKEY_MEDIA_STOP) && 211 (shift || ctrl || alt || command)) { 212 *error = ErrorUtils::FormatErrorMessageUTF16( 213 errors::kInvalidKeyBindingMediaKeyWithModifier, 214 base::IntToString(index), 215 platform_key, 216 accelerator); 217 return ui::Accelerator(); 218 } 219 220 return ui::Accelerator(key, modifiers); 221} 222 223// For Mac, we convert "Ctrl" to "Command" and "MacCtrl" to "Ctrl". Other 224// platforms leave the shortcut untouched. 225std::string NormalizeShortcutSuggestion(const std::string& suggestion, 226 const std::string& platform) { 227 bool normalize = false; 228 if (platform == values::kKeybindingPlatformMac) { 229 normalize = true; 230 } else if (platform == values::kKeybindingPlatformDefault) { 231#if defined(OS_MACOSX) 232 normalize = true; 233#endif 234 } 235 236 if (!normalize) 237 return suggestion; 238 239 std::vector<std::string> tokens; 240 base::SplitString(suggestion, '+', &tokens); 241 for (size_t i = 0; i < tokens.size(); i++) { 242 if (tokens[i] == values::kKeyCtrl) 243 tokens[i] = values::kKeyCommand; 244 else if (tokens[i] == values::kKeyMacCtrl) 245 tokens[i] = values::kKeyCtrl; 246 } 247 return JoinString(tokens, '+'); 248} 249 250} // namespace 251 252Command::Command() : global_(false) {} 253 254Command::Command(const std::string& command_name, 255 const base::string16& description, 256 const std::string& accelerator, 257 bool global) 258 : command_name_(command_name), 259 description_(description), 260 global_(global) { 261 base::string16 error; 262 accelerator_ = ParseImpl(accelerator, CommandPlatform(), 0, 263 IsNamedCommand(command_name), &error); 264} 265 266Command::~Command() {} 267 268// static 269std::string Command::CommandPlatform() { 270#if defined(OS_WIN) 271 return values::kKeybindingPlatformWin; 272#elif defined(OS_MACOSX) 273 return values::kKeybindingPlatformMac; 274#elif defined(OS_CHROMEOS) 275 return values::kKeybindingPlatformChromeOs; 276#elif defined(OS_LINUX) 277 return values::kKeybindingPlatformLinux; 278#else 279 return ""; 280#endif 281} 282 283// static 284ui::Accelerator Command::StringToAccelerator(const std::string& accelerator, 285 const std::string& command_name) { 286 base::string16 error; 287 ui::Accelerator parsed = 288 ParseImpl(accelerator, Command::CommandPlatform(), 0, 289 IsNamedCommand(command_name), &error); 290 return parsed; 291} 292 293// static 294std::string Command::AcceleratorToString(const ui::Accelerator& accelerator) { 295 std::string shortcut; 296 297 // Ctrl and Alt are mutually exclusive. 298 if (accelerator.IsCtrlDown()) 299 shortcut += values::kKeyCtrl; 300 else if (accelerator.IsAltDown()) 301 shortcut += values::kKeyAlt; 302 if (!shortcut.empty()) 303 shortcut += values::kKeySeparator; 304 305 if (accelerator.IsCmdDown()) { 306 shortcut += values::kKeyCommand; 307 shortcut += values::kKeySeparator; 308 } 309 310 if (accelerator.IsShiftDown()) { 311 shortcut += values::kKeyShift; 312 shortcut += values::kKeySeparator; 313 } 314 315 if (accelerator.key_code() >= ui::VKEY_0 && 316 accelerator.key_code() <= ui::VKEY_9) { 317 shortcut += '0' + (accelerator.key_code() - ui::VKEY_0); 318 } else if (accelerator.key_code() >= ui::VKEY_A && 319 accelerator.key_code() <= ui::VKEY_Z) { 320 shortcut += 'A' + (accelerator.key_code() - ui::VKEY_A); 321 } else { 322 switch (accelerator.key_code()) { 323 case ui::VKEY_OEM_COMMA: 324 shortcut += values::kKeyComma; 325 break; 326 case ui::VKEY_OEM_PERIOD: 327 shortcut += values::kKeyPeriod; 328 break; 329 case ui::VKEY_UP: 330 shortcut += values::kKeyUp; 331 break; 332 case ui::VKEY_DOWN: 333 shortcut += values::kKeyDown; 334 break; 335 case ui::VKEY_LEFT: 336 shortcut += values::kKeyLeft; 337 break; 338 case ui::VKEY_RIGHT: 339 shortcut += values::kKeyRight; 340 break; 341 case ui::VKEY_INSERT: 342 shortcut += values::kKeyIns; 343 break; 344 case ui::VKEY_DELETE: 345 shortcut += values::kKeyDel; 346 break; 347 case ui::VKEY_HOME: 348 shortcut += values::kKeyHome; 349 break; 350 case ui::VKEY_END: 351 shortcut += values::kKeyEnd; 352 break; 353 case ui::VKEY_PRIOR: 354 shortcut += values::kKeyPgUp; 355 break; 356 case ui::VKEY_NEXT: 357 shortcut += values::kKeyPgDwn; 358 break; 359 case ui::VKEY_TAB: 360 shortcut += values::kKeyTab; 361 break; 362 case ui::VKEY_MEDIA_NEXT_TRACK: 363 shortcut += values::kKeyMediaNextTrack; 364 break; 365 case ui::VKEY_MEDIA_PLAY_PAUSE: 366 shortcut += values::kKeyMediaPlayPause; 367 break; 368 case ui::VKEY_MEDIA_PREV_TRACK: 369 shortcut += values::kKeyMediaPrevTrack; 370 break; 371 case ui::VKEY_MEDIA_STOP: 372 shortcut += values::kKeyMediaStop; 373 break; 374 default: 375 return ""; 376 } 377 } 378 return shortcut; 379} 380 381// static 382bool Command::IsMediaKey(const ui::Accelerator& accelerator) { 383 if (accelerator.modifiers() != 0) 384 return false; 385 386 return (accelerator.key_code() == ui::VKEY_MEDIA_NEXT_TRACK || 387 accelerator.key_code() == ui::VKEY_MEDIA_PREV_TRACK || 388 accelerator.key_code() == ui::VKEY_MEDIA_PLAY_PAUSE || 389 accelerator.key_code() == ui::VKEY_MEDIA_STOP); 390} 391 392bool Command::Parse(const base::DictionaryValue* command, 393 const std::string& command_name, 394 int index, 395 base::string16* error) { 396 DCHECK(!command_name.empty()); 397 398 base::string16 description; 399 if (IsNamedCommand(command_name)) { 400 if (!command->GetString(keys::kDescription, &description) || 401 description.empty()) { 402 *error = ErrorUtils::FormatErrorMessageUTF16( 403 errors::kInvalidKeyBindingDescription, 404 base::IntToString(index)); 405 return false; 406 } 407 } 408 409 // We'll build up a map of platform-to-shortcut suggestions. 410 typedef std::map<const std::string, std::string> SuggestionMap; 411 SuggestionMap suggestions; 412 413 // First try to parse the |suggested_key| as a dictionary. 414 const base::DictionaryValue* suggested_key_dict; 415 if (command->GetDictionary(keys::kSuggestedKey, &suggested_key_dict)) { 416 for (base::DictionaryValue::Iterator iter(*suggested_key_dict); 417 !iter.IsAtEnd(); iter.Advance()) { 418 // For each item in the dictionary, extract the platforms specified. 419 std::string suggested_key_string; 420 if (iter.value().GetAsString(&suggested_key_string) && 421 !suggested_key_string.empty()) { 422 // Found a platform, add it to the suggestions list. 423 suggestions[iter.key()] = suggested_key_string; 424 } else { 425 *error = ErrorUtils::FormatErrorMessageUTF16( 426 errors::kInvalidKeyBinding, 427 base::IntToString(index), 428 keys::kSuggestedKey, 429 kMissing); 430 return false; 431 } 432 } 433 } else { 434 // No dictionary was found, fall back to using just a string, so developers 435 // don't have to specify a dictionary if they just want to use one default 436 // for all platforms. 437 std::string suggested_key_string; 438 if (command->GetString(keys::kSuggestedKey, &suggested_key_string) && 439 !suggested_key_string.empty()) { 440 // If only a single string is provided, it must be default for all. 441 suggestions[values::kKeybindingPlatformDefault] = suggested_key_string; 442 } else { 443 suggestions[values::kKeybindingPlatformDefault] = ""; 444 } 445 } 446 447 // Check if this is a global or a regular shortcut. 448 bool global = false; 449 command->GetBoolean(keys::kGlobal, &global); 450 451 // Normalize the suggestions. 452 for (SuggestionMap::iterator iter = suggestions.begin(); 453 iter != suggestions.end(); ++iter) { 454 // Before we normalize Ctrl to Command we must detect when the developer 455 // specified Command in the Default section, which will work on Mac after 456 // normalization but only fail on other platforms when they try it out on 457 // other platforms, which is not what we want. 458 if (iter->first == values::kKeybindingPlatformDefault && 459 iter->second.find("Command+") != std::string::npos) { 460 *error = ErrorUtils::FormatErrorMessageUTF16( 461 errors::kInvalidKeyBinding, 462 base::IntToString(index), 463 keys::kSuggestedKey, 464 kCommandKeyNotSupported); 465 return false; 466 } 467 468 suggestions[iter->first] = NormalizeShortcutSuggestion(iter->second, 469 iter->first); 470 } 471 472 std::string platform = CommandPlatform(); 473 std::string key = platform; 474 if (suggestions.find(key) == suggestions.end()) 475 key = values::kKeybindingPlatformDefault; 476 if (suggestions.find(key) == suggestions.end()) { 477 *error = ErrorUtils::FormatErrorMessageUTF16( 478 errors::kInvalidKeyBindingMissingPlatform, 479 base::IntToString(index), 480 keys::kSuggestedKey, 481 platform); 482 return false; // No platform specified and no fallback. Bail. 483 } 484 485 // For developer convenience, we parse all the suggestions (and complain about 486 // errors for platforms other than the current one) but use only what we need. 487 std::map<const std::string, std::string>::const_iterator iter = 488 suggestions.begin(); 489 for ( ; iter != suggestions.end(); ++iter) { 490 ui::Accelerator accelerator; 491 if (!iter->second.empty()) { 492 // Note that we pass iter->first to pretend we are on a platform we're not 493 // on. 494 accelerator = ParseImpl(iter->second, iter->first, index, 495 IsNamedCommand(command_name), error); 496 if (accelerator.key_code() == ui::VKEY_UNKNOWN) { 497 if (error->empty()) { 498 *error = ErrorUtils::FormatErrorMessageUTF16( 499 errors::kInvalidKeyBinding, 500 base::IntToString(index), 501 iter->first, 502 iter->second); 503 } 504 return false; 505 } 506 } 507 508 if (iter->first == key) { 509 // This platform is our platform, so grab this key. 510 accelerator_ = accelerator; 511 command_name_ = command_name; 512 description_ = description; 513 global_ = global; 514 } 515 } 516 return true; 517} 518 519base::DictionaryValue* Command::ToValue(const Extension* extension, 520 bool active) const { 521 base::DictionaryValue* extension_data = new base::DictionaryValue(); 522 523 base::string16 command_description; 524 bool extension_action = false; 525 if (command_name() == values::kBrowserActionCommandEvent || 526 command_name() == values::kPageActionCommandEvent) { 527 command_description = 528 l10n_util::GetStringUTF16(IDS_EXTENSION_COMMANDS_GENERIC_ACTIVATE); 529 extension_action = true; 530 } else { 531 command_description = description(); 532 } 533 extension_data->SetString("description", command_description); 534 extension_data->SetBoolean("active", active); 535 extension_data->SetString("keybinding", accelerator().GetShortcutText()); 536 extension_data->SetString("command_name", command_name()); 537 extension_data->SetString("extension_id", extension->id()); 538 extension_data->SetBoolean("global", global()); 539 extension_data->SetBoolean("extension_action", extension_action); 540 return extension_data; 541} 542 543} // namespace extensions 544