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