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
;
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
34 static const int kMaxTokenSize
= 4;
36 static const int kMaxTokenSize
= 3;
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
,
62 bool should_parse_media_keys
,
63 base::string16
* error
) {
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
),
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
),
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
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
;
112 // No other platform supports Command.
113 key
= ui::VKEY_UNKNOWN
;
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
;
121 // No other platform supports Search.
122 key
= ui::VKEY_UNKNOWN
;
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
;
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
) {
160 } else if (tokens
[i
] == values::kKeyDown
) {
162 } else if (tokens
[i
] == values::kKeyLeft
) {
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
) {
172 } else if (tokens
[i
] == values::kKeyEnd
) {
174 } else if (tokens
[i
] == values::kKeyPgUp
) {
175 key
= ui::VKEY_PRIOR
;
176 } else if (tokens
[i
] == values::kKeyPgDwn
) {
178 } else if (tokens
[i
] == values::kKeySpace
) {
179 key
= ui::VKEY_SPACE
;
180 } else if (tokens
[i
] == values::kKeyTab
) {
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'));
201 key
= ui::VKEY_UNKNOWN
;
205 *error
= ErrorUtils::FormatErrorMessageUTF16(
206 errors::kInvalidKeyBinding
,
207 base::IntToString(index
),
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,
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
),
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
),
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
) {
258 } else if (platform
== values::kKeybindingPlatformDefault
) {
259 #if defined(OS_MACOSX)
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
, "+");
280 Command::Command() : global_(false) {}
282 Command::Command(const std::string
& command_name
,
283 const base::string16
& description
,
284 const std::string
& accelerator
,
286 : command_name_(command_name
),
287 description_(description
),
289 base::string16 error
;
290 accelerator_
= ParseImpl(accelerator
, CommandPlatform(), 0,
291 IsNamedCommand(command_name
), &error
);
294 Command::~Command() {}
297 std::string
Command::CommandPlatform() {
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
;
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
);
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
;
338 shortcut
+= values::kKeyCommand
;
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
);
355 switch (accelerator
.key_code()) {
356 case ui::VKEY_OEM_COMMA
:
357 shortcut
+= values::kKeyComma
;
359 case ui::VKEY_OEM_PERIOD
:
360 shortcut
+= values::kKeyPeriod
;
363 shortcut
+= values::kKeyUp
;
366 shortcut
+= values::kKeyDown
;
369 shortcut
+= values::kKeyLeft
;
372 shortcut
+= values::kKeyRight
;
374 case ui::VKEY_INSERT
:
375 shortcut
+= values::kKeyIns
;
377 case ui::VKEY_DELETE
:
378 shortcut
+= values::kKeyDel
;
381 shortcut
+= values::kKeyHome
;
384 shortcut
+= values::kKeyEnd
;
387 shortcut
+= values::kKeyPgUp
;
390 shortcut
+= values::kKeyPgDwn
;
393 shortcut
+= values::kKeySpace
;
396 shortcut
+= values::kKeyTab
;
398 case ui::VKEY_MEDIA_NEXT_TRACK
:
399 shortcut
+= values::kKeyMediaNextTrack
;
401 case ui::VKEY_MEDIA_PLAY_PAUSE
:
402 shortcut
+= values::kKeyMediaPlayPause
;
404 case ui::VKEY_MEDIA_PREV_TRACK
:
405 shortcut
+= values::kKeyMediaPrevTrack
;
407 case ui::VKEY_MEDIA_STOP
:
408 shortcut
+= values::kKeyMediaStop
;
418 bool Command::IsMediaKey(const ui::Accelerator
& accelerator
) {
419 if (accelerator
.modifiers() != 0)
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
,
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
));
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
;
461 *error
= ErrorUtils::FormatErrorMessageUTF16(
462 errors::kInvalidKeyBinding
,
463 base::IntToString(index
),
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
;
479 suggestions
[values::kKeybindingPlatformDefault
] = "";
483 // Check if this is a global or a regular shortcut.
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
),
500 kCommandKeyNotSupported
);
504 suggestions
[iter
->first
] = NormalizeShortcutSuggestion(iter
->second
,
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
),
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
=
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
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
),
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
;
555 } // namespace extensions