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 bool IsNamedCommand(const std::string
& command_name
) {
32 return command_name
!= values::kPageActionCommandEvent
&&
33 command_name
!= values::kBrowserActionCommandEvent
;
36 bool DoesRequireModifier(const std::string
& accelerator
) {
37 return accelerator
!= values::kKeyMediaNextTrack
&&
38 accelerator
!= values::kKeyMediaPlayPause
&&
39 accelerator
!= values::kKeyMediaPrevTrack
&&
40 accelerator
!= values::kKeyMediaStop
;
43 ui::Accelerator
ParseImpl(const std::string
& accelerator
,
44 const std::string
& platform_key
,
46 bool should_parse_media_keys
,
47 base::string16
* error
) {
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
),
58 return ui::Accelerator();
61 std::vector
<std::string
> tokens
;
62 base::SplitString(accelerator
, '+', &tokens
);
63 if (tokens
.size() == 0 ||
64 (tokens
.size() == 1 && DoesRequireModifier(accelerator
)) ||
66 *error
= ErrorUtils::FormatErrorMessageUTF16(
67 errors::kInvalidKeyBinding
,
68 base::IntToString(index
),
71 return ui::Accelerator();
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
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
;
96 // No other platform supports Command.
97 key
= ui::VKEY_UNKNOWN
;
100 } else if (tokens
[i
] == values::kKeySearch
) {
101 // Search is a special modifier only on ChromeOS and maps to 'Command'.
102 if (platform_key
== values::kKeybindingPlatformChromeOs
) {
103 modifiers
|= ui::EF_COMMAND_DOWN
;
105 // No other platform supports Search.
106 key
= ui::VKEY_UNKNOWN
;
109 } else if (tokens
[i
] == values::kKeyAlt
) {
110 modifiers
|= ui::EF_ALT_DOWN
;
111 } else if (tokens
[i
] == values::kKeyShift
) {
112 modifiers
|= ui::EF_SHIFT_DOWN
;
113 } else if (tokens
[i
].size() == 1 || // A-Z, 0-9.
114 tokens
[i
] == values::kKeyComma
||
115 tokens
[i
] == values::kKeyPeriod
||
116 tokens
[i
] == values::kKeyUp
||
117 tokens
[i
] == values::kKeyDown
||
118 tokens
[i
] == values::kKeyLeft
||
119 tokens
[i
] == values::kKeyRight
||
120 tokens
[i
] == values::kKeyIns
||
121 tokens
[i
] == values::kKeyDel
||
122 tokens
[i
] == values::kKeyHome
||
123 tokens
[i
] == values::kKeyEnd
||
124 tokens
[i
] == values::kKeyPgUp
||
125 tokens
[i
] == values::kKeyPgDwn
||
126 tokens
[i
] == values::kKeyTab
||
127 tokens
[i
] == values::kKeyMediaNextTrack
||
128 tokens
[i
] == values::kKeyMediaPlayPause
||
129 tokens
[i
] == values::kKeyMediaPrevTrack
||
130 tokens
[i
] == values::kKeyMediaStop
) {
131 if (key
!= ui::VKEY_UNKNOWN
) {
132 // Multiple key assignments.
133 key
= ui::VKEY_UNKNOWN
;
137 if (tokens
[i
] == values::kKeyComma
) {
138 key
= ui::VKEY_OEM_COMMA
;
139 } else if (tokens
[i
] == values::kKeyPeriod
) {
140 key
= ui::VKEY_OEM_PERIOD
;
141 } else if (tokens
[i
] == values::kKeyUp
) {
143 } else if (tokens
[i
] == values::kKeyDown
) {
145 } else if (tokens
[i
] == values::kKeyLeft
) {
147 } else if (tokens
[i
] == values::kKeyRight
) {
148 key
= ui::VKEY_RIGHT
;
149 } else if (tokens
[i
] == values::kKeyIns
) {
150 key
= ui::VKEY_INSERT
;
151 } else if (tokens
[i
] == values::kKeyDel
) {
152 key
= ui::VKEY_DELETE
;
153 } else if (tokens
[i
] == values::kKeyHome
) {
155 } else if (tokens
[i
] == values::kKeyEnd
) {
157 } else if (tokens
[i
] == values::kKeyPgUp
) {
158 key
= ui::VKEY_PRIOR
;
159 } else if (tokens
[i
] == values::kKeyPgDwn
) {
161 } else if (tokens
[i
] == values::kKeyTab
) {
163 } else if (tokens
[i
] == values::kKeyMediaNextTrack
&&
164 should_parse_media_keys
) {
165 key
= ui::VKEY_MEDIA_NEXT_TRACK
;
166 } else if (tokens
[i
] == values::kKeyMediaPlayPause
&&
167 should_parse_media_keys
) {
168 key
= ui::VKEY_MEDIA_PLAY_PAUSE
;
169 } else if (tokens
[i
] == values::kKeyMediaPrevTrack
&&
170 should_parse_media_keys
) {
171 key
= ui::VKEY_MEDIA_PREV_TRACK
;
172 } else if (tokens
[i
] == values::kKeyMediaStop
&&
173 should_parse_media_keys
) {
174 key
= ui::VKEY_MEDIA_STOP
;
175 } else if (tokens
[i
].size() == 1 &&
176 tokens
[i
][0] >= 'A' && tokens
[i
][0] <= 'Z') {
177 key
= static_cast<ui::KeyboardCode
>(ui::VKEY_A
+ (tokens
[i
][0] - 'A'));
178 } else if (tokens
[i
].size() == 1 &&
179 tokens
[i
][0] >= '0' && tokens
[i
][0] <= '9') {
180 key
= static_cast<ui::KeyboardCode
>(ui::VKEY_0
+ (tokens
[i
][0] - '0'));
182 key
= ui::VKEY_UNKNOWN
;
186 *error
= ErrorUtils::FormatErrorMessageUTF16(
187 errors::kInvalidKeyBinding
,
188 base::IntToString(index
),
191 return ui::Accelerator();
195 bool command
= (modifiers
& ui::EF_COMMAND_DOWN
) != 0;
196 bool ctrl
= (modifiers
& ui::EF_CONTROL_DOWN
) != 0;
197 bool alt
= (modifiers
& ui::EF_ALT_DOWN
) != 0;
198 bool shift
= (modifiers
& ui::EF_SHIFT_DOWN
) != 0;
200 // We support Ctrl+foo, Alt+foo, Ctrl+Shift+foo, Alt+Shift+foo, but not
201 // Ctrl+Alt+foo and not Shift+foo either. For a more detailed reason why we
202 // don't support Ctrl+Alt+foo see this article:
203 // http://blogs.msdn.com/b/oldnewthing/archive/2004/03/29/101121.aspx.
204 // On Mac Command can also be used in combination with Shift or on its own,
206 if (key
== ui::VKEY_UNKNOWN
|| (ctrl
&& alt
) || (command
&& alt
) ||
207 (shift
&& !ctrl
&& !alt
&& !command
)) {
208 *error
= ErrorUtils::FormatErrorMessageUTF16(
209 errors::kInvalidKeyBinding
,
210 base::IntToString(index
),
213 return ui::Accelerator();
216 if ((key
== ui::VKEY_MEDIA_NEXT_TRACK
||
217 key
== ui::VKEY_MEDIA_PREV_TRACK
||
218 key
== ui::VKEY_MEDIA_PLAY_PAUSE
||
219 key
== ui::VKEY_MEDIA_STOP
) &&
220 (shift
|| ctrl
|| alt
|| command
)) {
221 *error
= ErrorUtils::FormatErrorMessageUTF16(
222 errors::kInvalidKeyBindingMediaKeyWithModifier
,
223 base::IntToString(index
),
226 return ui::Accelerator();
229 return ui::Accelerator(key
, modifiers
);
232 // For Mac, we convert "Ctrl" to "Command" and "MacCtrl" to "Ctrl". Other
233 // platforms leave the shortcut untouched.
234 std::string
NormalizeShortcutSuggestion(const std::string
& suggestion
,
235 const std::string
& platform
) {
236 bool normalize
= false;
237 if (platform
== values::kKeybindingPlatformMac
) {
239 } else if (platform
== values::kKeybindingPlatformDefault
) {
240 #if defined(OS_MACOSX)
248 std::vector
<std::string
> tokens
;
249 base::SplitString(suggestion
, '+', &tokens
);
250 for (size_t i
= 0; i
< tokens
.size(); i
++) {
251 if (tokens
[i
] == values::kKeyCtrl
)
252 tokens
[i
] = values::kKeyCommand
;
253 else if (tokens
[i
] == values::kKeyMacCtrl
)
254 tokens
[i
] = values::kKeyCtrl
;
256 return JoinString(tokens
, '+');
261 Command::Command() : global_(false) {}
263 Command::Command(const std::string
& command_name
,
264 const base::string16
& description
,
265 const std::string
& accelerator
,
267 : command_name_(command_name
),
268 description_(description
),
270 base::string16 error
;
271 accelerator_
= ParseImpl(accelerator
, CommandPlatform(), 0,
272 IsNamedCommand(command_name
), &error
);
275 Command::~Command() {}
278 std::string
Command::CommandPlatform() {
280 return values::kKeybindingPlatformWin
;
281 #elif defined(OS_MACOSX)
282 return values::kKeybindingPlatformMac
;
283 #elif defined(OS_CHROMEOS)
284 return values::kKeybindingPlatformChromeOs
;
285 #elif defined(OS_LINUX)
286 return values::kKeybindingPlatformLinux
;
293 ui::Accelerator
Command::StringToAccelerator(const std::string
& accelerator
,
294 const std::string
& command_name
) {
295 base::string16 error
;
296 ui::Accelerator parsed
=
297 ParseImpl(accelerator
, Command::CommandPlatform(), 0,
298 IsNamedCommand(command_name
), &error
);
303 std::string
Command::AcceleratorToString(const ui::Accelerator
& accelerator
) {
304 std::string shortcut
;
306 // Ctrl and Alt are mutually exclusive.
307 if (accelerator
.IsCtrlDown())
308 shortcut
+= values::kKeyCtrl
;
309 else if (accelerator
.IsAltDown())
310 shortcut
+= values::kKeyAlt
;
311 if (!shortcut
.empty())
312 shortcut
+= values::kKeySeparator
;
314 if (accelerator
.IsCmdDown()) {
315 #if defined(OS_CHROMEOS)
316 // Chrome OS treats the Search key like the Command key.
317 shortcut
+= values::kKeySearch
;
319 shortcut
+= values::kKeyCommand
;
321 shortcut
+= values::kKeySeparator
;
324 if (accelerator
.IsShiftDown()) {
325 shortcut
+= values::kKeyShift
;
326 shortcut
+= values::kKeySeparator
;
329 if (accelerator
.key_code() >= ui::VKEY_0
&&
330 accelerator
.key_code() <= ui::VKEY_9
) {
331 shortcut
+= '0' + (accelerator
.key_code() - ui::VKEY_0
);
332 } else if (accelerator
.key_code() >= ui::VKEY_A
&&
333 accelerator
.key_code() <= ui::VKEY_Z
) {
334 shortcut
+= 'A' + (accelerator
.key_code() - ui::VKEY_A
);
336 switch (accelerator
.key_code()) {
337 case ui::VKEY_OEM_COMMA
:
338 shortcut
+= values::kKeyComma
;
340 case ui::VKEY_OEM_PERIOD
:
341 shortcut
+= values::kKeyPeriod
;
344 shortcut
+= values::kKeyUp
;
347 shortcut
+= values::kKeyDown
;
350 shortcut
+= values::kKeyLeft
;
353 shortcut
+= values::kKeyRight
;
355 case ui::VKEY_INSERT
:
356 shortcut
+= values::kKeyIns
;
358 case ui::VKEY_DELETE
:
359 shortcut
+= values::kKeyDel
;
362 shortcut
+= values::kKeyHome
;
365 shortcut
+= values::kKeyEnd
;
368 shortcut
+= values::kKeyPgUp
;
371 shortcut
+= values::kKeyPgDwn
;
374 shortcut
+= values::kKeyTab
;
376 case ui::VKEY_MEDIA_NEXT_TRACK
:
377 shortcut
+= values::kKeyMediaNextTrack
;
379 case ui::VKEY_MEDIA_PLAY_PAUSE
:
380 shortcut
+= values::kKeyMediaPlayPause
;
382 case ui::VKEY_MEDIA_PREV_TRACK
:
383 shortcut
+= values::kKeyMediaPrevTrack
;
385 case ui::VKEY_MEDIA_STOP
:
386 shortcut
+= values::kKeyMediaStop
;
396 bool Command::IsMediaKey(const ui::Accelerator
& accelerator
) {
397 if (accelerator
.modifiers() != 0)
400 return (accelerator
.key_code() == ui::VKEY_MEDIA_NEXT_TRACK
||
401 accelerator
.key_code() == ui::VKEY_MEDIA_PREV_TRACK
||
402 accelerator
.key_code() == ui::VKEY_MEDIA_PLAY_PAUSE
||
403 accelerator
.key_code() == ui::VKEY_MEDIA_STOP
);
406 bool Command::Parse(const base::DictionaryValue
* command
,
407 const std::string
& command_name
,
409 base::string16
* error
) {
410 DCHECK(!command_name
.empty());
412 base::string16 description
;
413 if (IsNamedCommand(command_name
)) {
414 if (!command
->GetString(keys::kDescription
, &description
) ||
415 description
.empty()) {
416 *error
= ErrorUtils::FormatErrorMessageUTF16(
417 errors::kInvalidKeyBindingDescription
,
418 base::IntToString(index
));
423 // We'll build up a map of platform-to-shortcut suggestions.
424 typedef std::map
<const std::string
, std::string
> SuggestionMap
;
425 SuggestionMap suggestions
;
427 // First try to parse the |suggested_key| as a dictionary.
428 const base::DictionaryValue
* suggested_key_dict
;
429 if (command
->GetDictionary(keys::kSuggestedKey
, &suggested_key_dict
)) {
430 for (base::DictionaryValue::Iterator
iter(*suggested_key_dict
);
431 !iter
.IsAtEnd(); iter
.Advance()) {
432 // For each item in the dictionary, extract the platforms specified.
433 std::string suggested_key_string
;
434 if (iter
.value().GetAsString(&suggested_key_string
) &&
435 !suggested_key_string
.empty()) {
436 // Found a platform, add it to the suggestions list.
437 suggestions
[iter
.key()] = suggested_key_string
;
439 *error
= ErrorUtils::FormatErrorMessageUTF16(
440 errors::kInvalidKeyBinding
,
441 base::IntToString(index
),
448 // No dictionary was found, fall back to using just a string, so developers
449 // don't have to specify a dictionary if they just want to use one default
450 // for all platforms.
451 std::string suggested_key_string
;
452 if (command
->GetString(keys::kSuggestedKey
, &suggested_key_string
) &&
453 !suggested_key_string
.empty()) {
454 // If only a single string is provided, it must be default for all.
455 suggestions
[values::kKeybindingPlatformDefault
] = suggested_key_string
;
457 suggestions
[values::kKeybindingPlatformDefault
] = "";
461 // Check if this is a global or a regular shortcut.
463 command
->GetBoolean(keys::kGlobal
, &global
);
465 // Normalize the suggestions.
466 for (SuggestionMap::iterator iter
= suggestions
.begin();
467 iter
!= suggestions
.end(); ++iter
) {
468 // Before we normalize Ctrl to Command we must detect when the developer
469 // specified Command in the Default section, which will work on Mac after
470 // normalization but only fail on other platforms when they try it out on
471 // other platforms, which is not what we want.
472 if (iter
->first
== values::kKeybindingPlatformDefault
&&
473 iter
->second
.find("Command+") != std::string::npos
) {
474 *error
= ErrorUtils::FormatErrorMessageUTF16(
475 errors::kInvalidKeyBinding
,
476 base::IntToString(index
),
478 kCommandKeyNotSupported
);
482 suggestions
[iter
->first
] = NormalizeShortcutSuggestion(iter
->second
,
486 std::string platform
= CommandPlatform();
487 std::string key
= platform
;
488 if (suggestions
.find(key
) == suggestions
.end())
489 key
= values::kKeybindingPlatformDefault
;
490 if (suggestions
.find(key
) == suggestions
.end()) {
491 *error
= ErrorUtils::FormatErrorMessageUTF16(
492 errors::kInvalidKeyBindingMissingPlatform
,
493 base::IntToString(index
),
496 return false; // No platform specified and no fallback. Bail.
499 // For developer convenience, we parse all the suggestions (and complain about
500 // errors for platforms other than the current one) but use only what we need.
501 std::map
<const std::string
, std::string
>::const_iterator iter
=
503 for ( ; iter
!= suggestions
.end(); ++iter
) {
504 ui::Accelerator accelerator
;
505 if (!iter
->second
.empty()) {
506 // Note that we pass iter->first to pretend we are on a platform we're not
508 accelerator
= ParseImpl(iter
->second
, iter
->first
, index
,
509 IsNamedCommand(command_name
), error
);
510 if (accelerator
.key_code() == ui::VKEY_UNKNOWN
) {
511 if (error
->empty()) {
512 *error
= ErrorUtils::FormatErrorMessageUTF16(
513 errors::kInvalidKeyBinding
,
514 base::IntToString(index
),
522 if (iter
->first
== key
) {
523 // This platform is our platform, so grab this key.
524 accelerator_
= accelerator
;
525 command_name_
= command_name
;
526 description_
= description
;
533 base::DictionaryValue
* Command::ToValue(const Extension
* extension
,
535 base::DictionaryValue
* extension_data
= new base::DictionaryValue();
537 base::string16 command_description
;
538 bool extension_action
= false;
539 if (command_name() == values::kBrowserActionCommandEvent
||
540 command_name() == values::kPageActionCommandEvent
) {
541 command_description
=
542 l10n_util::GetStringUTF16(IDS_EXTENSION_COMMANDS_GENERIC_ACTIVATE
);
543 extension_action
= true;
545 command_description
= description();
547 extension_data
->SetString("description", command_description
);
548 extension_data
->SetBoolean("active", active
);
549 extension_data
->SetString("keybinding", accelerator().GetShortcutText());
550 extension_data
->SetString("command_name", command_name());
551 extension_data
->SetString("extension_id", extension
->id());
552 extension_data
->SetBoolean("global", global());
553 extension_data
->SetBoolean("extension_action", extension_action
);
554 return extension_data
;
557 } // namespace extensions