command.cc revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
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/string_util.h" 9#include "base/strings/string_number_conversions.h" 10#include "base/strings/string_split.h" 11#include "base/values.h" 12#include "chrome/common/extensions/extension.h" 13#include "chrome/common/extensions/extension_manifest_constants.h" 14#include "extensions/common/error_utils.h" 15#include "grit/generated_resources.h" 16#include "ui/base/l10n/l10n_util.h" 17 18namespace errors = extension_manifest_errors; 19namespace keys = extension_manifest_keys; 20namespace values = extension_manifest_values; 21 22using extensions::ErrorUtils; 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 31 32ui::Accelerator ParseImpl(const std::string& accelerator, 33 const std::string& platform_key, 34 int index, 35 string16* error) { 36 if (platform_key != values::kKeybindingPlatformWin && 37 platform_key != values::kKeybindingPlatformMac && 38 platform_key != values::kKeybindingPlatformChromeOs && 39 platform_key != values::kKeybindingPlatformLinux && 40 platform_key != values::kKeybindingPlatformDefault) { 41 *error = ErrorUtils::FormatErrorMessageUTF16( 42 errors::kInvalidKeyBindingUnknownPlatform, 43 base::IntToString(index), 44 platform_key); 45 return ui::Accelerator(); 46 } 47 48 std::vector<std::string> tokens; 49 base::SplitString(accelerator, '+', &tokens); 50 if (tokens.size() < 2 || tokens.size() > 3) { 51 *error = ErrorUtils::FormatErrorMessageUTF16( 52 errors::kInvalidKeyBinding, 53 base::IntToString(index), 54 platform_key, 55 accelerator); 56 return ui::Accelerator(); 57 } 58 59 // Now, parse it into an accelerator. 60 int modifiers = ui::EF_NONE; 61 ui::KeyboardCode key = ui::VKEY_UNKNOWN; 62 for (size_t i = 0; i < tokens.size(); i++) { 63 if (tokens[i] == "Ctrl") { 64 modifiers |= ui::EF_CONTROL_DOWN; 65 } else if (tokens[i] == "Command") { 66 if (platform_key == "mac") { 67 // Either the developer specified Command+foo in the manifest for Mac or 68 // they specified Ctrl and it got normalized to Command (to get Ctrl on 69 // Mac the developer has to specify MacCtrl). Therefore we treat this 70 // as Command. 71 modifiers |= ui::EF_COMMAND_DOWN; 72#if defined(OS_MACOSX) 73 } else if (platform_key == "default") { 74 // If we see "Command+foo" in the Default section it can mean two 75 // things, depending on the platform: 76 // The developer specified "Ctrl+foo" for Default and it got normalized 77 // on Mac to "Command+foo". This is fine. Treat it as Command. 78 modifiers |= ui::EF_COMMAND_DOWN; 79#endif 80 } else { 81 // No other platform supports Command. 82 key = ui::VKEY_UNKNOWN; 83 break; 84 } 85 } else if (tokens[i] == "Alt") { 86 modifiers |= ui::EF_ALT_DOWN; 87 } else if (tokens[i] == "Shift") { 88 modifiers |= ui::EF_SHIFT_DOWN; 89 } else if (tokens[i].size() == 1) { 90 if (key != ui::VKEY_UNKNOWN) { 91 // Multiple key assignments. 92 key = ui::VKEY_UNKNOWN; 93 break; 94 } 95 if (tokens[i][0] >= 'A' && tokens[i][0] <= 'Z') { 96 key = static_cast<ui::KeyboardCode>(ui::VKEY_A + (tokens[i][0] - 'A')); 97 } else if (tokens[i][0] >= '0' && tokens[i][0] <= '9') { 98 key = static_cast<ui::KeyboardCode>(ui::VKEY_0 + (tokens[i][0] - '0')); 99 } else { 100 key = ui::VKEY_UNKNOWN; 101 break; 102 } 103 } else { 104 *error = ErrorUtils::FormatErrorMessageUTF16( 105 errors::kInvalidKeyBinding, 106 base::IntToString(index), 107 platform_key, 108 accelerator); 109 return ui::Accelerator(); 110 } 111 } 112 bool command = (modifiers & ui::EF_COMMAND_DOWN) != 0; 113 bool ctrl = (modifiers & ui::EF_CONTROL_DOWN) != 0; 114 bool alt = (modifiers & ui::EF_ALT_DOWN) != 0; 115 bool shift = (modifiers & ui::EF_SHIFT_DOWN) != 0; 116 117 // We support Ctrl+foo, Alt+foo, Ctrl+Shift+foo, Alt+Shift+foo, but not 118 // Ctrl+Alt+foo and not Shift+foo either. For a more detailed reason why we 119 // don't support Ctrl+Alt+foo see this article: 120 // http://blogs.msdn.com/b/oldnewthing/archive/2004/03/29/101121.aspx. 121 // On Mac Command can also be used in combination with Shift or on its own, 122 // as a modifier. 123 if (key == ui::VKEY_UNKNOWN || (ctrl && alt) || (command && alt) || 124 (shift && !ctrl && !alt && !command)) { 125 *error = ErrorUtils::FormatErrorMessageUTF16( 126 errors::kInvalidKeyBinding, 127 base::IntToString(index), 128 platform_key, 129 accelerator); 130 return ui::Accelerator(); 131 } 132 133 return ui::Accelerator(key, modifiers); 134} 135 136// For Mac, we convert "Ctrl" to "Command" and "MacCtrl" to "Ctrl". Other 137// platforms leave the shortcut untouched. 138std::string NormalizeShortcutSuggestion(const std::string& suggestion, 139 const std::string& platform) { 140 bool normalize = false; 141 if (platform == "mac") { 142 normalize = true; 143 } else if (platform == "default") { 144#if defined(OS_MACOSX) 145 normalize = true; 146#endif 147 } 148 149 if (!normalize) 150 return suggestion; 151 152 std::string key; 153 std::vector<std::string> tokens; 154 base::SplitString(suggestion, '+', &tokens); 155 for (size_t i = 0; i < tokens.size(); i++) { 156 if (tokens[i] == "Ctrl") 157 tokens[i] = "Command"; 158 else if (tokens[i] == "MacCtrl") 159 tokens[i] = "Ctrl"; 160 } 161 return JoinString(tokens, '+'); 162} 163 164} // namespace 165 166namespace extensions { 167 168Command::Command() {} 169 170Command::Command(const std::string& command_name, 171 const string16& description, 172 const std::string& accelerator) 173 : command_name_(command_name), 174 description_(description) { 175 string16 error; 176 accelerator_ = ParseImpl(accelerator, CommandPlatform(), 0, &error); 177} 178 179Command::~Command() {} 180 181// static 182std::string Command::CommandPlatform() { 183#if defined(OS_WIN) 184 return values::kKeybindingPlatformWin; 185#elif defined(OS_MACOSX) 186 return values::kKeybindingPlatformMac; 187#elif defined(OS_CHROMEOS) 188 return values::kKeybindingPlatformChromeOs; 189#elif defined(OS_LINUX) 190 return values::kKeybindingPlatformLinux; 191#else 192 return ""; 193#endif 194} 195 196// static 197ui::Accelerator Command::StringToAccelerator(const std::string& accelerator) { 198 string16 error; 199 Command command; 200 ui::Accelerator parsed = 201 ParseImpl(accelerator, Command::CommandPlatform(), 0, &error); 202 return parsed; 203} 204 205bool Command::Parse(const DictionaryValue* command, 206 const std::string& command_name, 207 int index, 208 string16* error) { 209 DCHECK(!command_name.empty()); 210 211 // We'll build up a map of platform-to-shortcut suggestions. 212 typedef std::map<const std::string, std::string> SuggestionMap; 213 SuggestionMap suggestions; 214 215 // First try to parse the |suggested_key| as a dictionary. 216 const DictionaryValue* suggested_key_dict; 217 if (command->GetDictionary(keys::kSuggestedKey, &suggested_key_dict)) { 218 for (DictionaryValue::Iterator iter(*suggested_key_dict); !iter.IsAtEnd(); 219 iter.Advance()) { 220 // For each item in the dictionary, extract the platforms specified. 221 std::string suggested_key_string; 222 if (iter.value().GetAsString(&suggested_key_string) && 223 !suggested_key_string.empty()) { 224 // Found a platform, add it to the suggestions list. 225 suggestions[iter.key()] = suggested_key_string; 226 } else { 227 *error = ErrorUtils::FormatErrorMessageUTF16( 228 errors::kInvalidKeyBinding, 229 base::IntToString(index), 230 keys::kSuggestedKey, 231 kMissing); 232 return false; 233 } 234 } 235 } else { 236 // No dictionary was found, fall back to using just a string, so developers 237 // don't have to specify a dictionary if they just want to use one default 238 // for all platforms. 239 std::string suggested_key_string; 240 if (command->GetString(keys::kSuggestedKey, &suggested_key_string) && 241 !suggested_key_string.empty()) { 242 // If only a single string is provided, it must be default for all. 243 suggestions["default"] = suggested_key_string; 244 } else { 245 *error = ErrorUtils::FormatErrorMessageUTF16( 246 errors::kInvalidKeyBinding, 247 base::IntToString(index), 248 keys::kSuggestedKey, 249 kMissing); 250 return false; 251 } 252 } 253 254 // Normalize the suggestions. 255 for (SuggestionMap::iterator iter = suggestions.begin(); 256 iter != suggestions.end(); ++iter) { 257 // Before we normalize Ctrl to Command we must detect when the developer 258 // specified Command in the Default section, which will work on Mac after 259 // normalization but only fail on other platforms when they try it out on 260 // other platforms, which is not what we want. 261 if (iter->first == "default" && 262 iter->second.find("Command+") != std::string::npos) { 263 *error = ErrorUtils::FormatErrorMessageUTF16( 264 errors::kInvalidKeyBinding, 265 base::IntToString(index), 266 keys::kSuggestedKey, 267 kCommandKeyNotSupported); 268 return false; 269 } 270 271 suggestions[iter->first] = NormalizeShortcutSuggestion(iter->second, 272 iter->first); 273 } 274 275 std::string platform = CommandPlatform(); 276 std::string key = platform; 277 if (suggestions.find(key) == suggestions.end()) 278 key = values::kKeybindingPlatformDefault; 279 if (suggestions.find(key) == suggestions.end()) { 280 *error = ErrorUtils::FormatErrorMessageUTF16( 281 errors::kInvalidKeyBindingMissingPlatform, 282 base::IntToString(index), 283 keys::kSuggestedKey, 284 platform); 285 return false; // No platform specified and no fallback. Bail. 286 } 287 288 // For developer convenience, we parse all the suggestions (and complain about 289 // errors for platforms other than the current one) but use only what we need. 290 std::map<const std::string, std::string>::const_iterator iter = 291 suggestions.begin(); 292 for ( ; iter != suggestions.end(); ++iter) { 293 // Note that we pass iter->first to pretend we are on a platform we're not 294 // on. 295 ui::Accelerator accelerator = 296 ParseImpl(iter->second, iter->first, index, error); 297 if (accelerator.key_code() == ui::VKEY_UNKNOWN) { 298 *error = ErrorUtils::FormatErrorMessageUTF16( 299 errors::kInvalidKeyBinding, 300 base::IntToString(index), 301 iter->first, 302 iter->second); 303 return false; 304 } 305 306 if (iter->first == key) { 307 // This platform is our platform, so grab this key. 308 accelerator_ = accelerator; 309 command_name_ = command_name; 310 311 if (command_name != 312 extension_manifest_values::kPageActionCommandEvent && 313 command_name != 314 extension_manifest_values::kBrowserActionCommandEvent && 315 command_name != 316 extension_manifest_values::kScriptBadgeCommandEvent) { 317 if (!command->GetString(keys::kDescription, &description_) || 318 description_.empty()) { 319 *error = ErrorUtils::FormatErrorMessageUTF16( 320 errors::kInvalidKeyBindingDescription, 321 base::IntToString(index)); 322 return false; 323 } 324 } 325 } 326 } 327 return true; 328} 329 330DictionaryValue* Command::ToValue(const Extension* extension, 331 bool active) const { 332 DictionaryValue* extension_data = new DictionaryValue(); 333 334 string16 command_description; 335 if (command_name() == values::kBrowserActionCommandEvent || 336 command_name() == values::kPageActionCommandEvent || 337 command_name() == values::kScriptBadgeCommandEvent) { 338 command_description = 339 l10n_util::GetStringUTF16(IDS_EXTENSION_COMMANDS_GENERIC_ACTIVATE); 340 } else { 341 command_description = description(); 342 } 343 extension_data->SetString("description", command_description); 344 extension_data->SetBoolean("active", active); 345 extension_data->SetString("keybinding", accelerator().GetShortcutText()); 346 extension_data->SetString("command_name", command_name()); 347 extension_data->SetString("extension_id", extension->id()); 348 349 return extension_data; 350} 351 352} // namespace extensions 353