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/common/chrome_version_info.h" // TODO(finnur): Remove.
13 #include "extensions/common/error_utils.h"
14 #include "extensions/common/extension.h"
15 #include "extensions/common/feature_switch.h"
16 #include "extensions/common/manifest_constants.h"
17 #include "grit/generated_resources.h"
18 #include "ui/base/l10n/l10n_util.h"
20 namespace extensions
{
22 namespace errors
= manifest_errors
;
23 namespace keys
= manifest_keys
;
24 namespace values
= manifest_values
;
28 static const char kMissing
[] = "Missing";
30 static const char kCommandKeyNotSupported
[] =
31 "Command key is not supported. Note: Ctrl means Command on Mac";
33 bool IsNamedCommand(const std::string
& command_name
) {
34 return command_name
!= values::kPageActionCommandEvent
&&
35 command_name
!= values::kBrowserActionCommandEvent
&&
36 command_name
!= values::kScriptBadgeCommandEvent
;
39 bool DoesRequireModifier(const std::string
& accelerator
) {
40 return accelerator
!= values::kKeyMediaNextTrack
&&
41 accelerator
!= values::kKeyMediaPlayPause
&&
42 accelerator
!= values::kKeyMediaPrevTrack
&&
43 accelerator
!= values::kKeyMediaStop
;
46 ui::Accelerator
ParseImpl(const std::string
& accelerator
,
47 const std::string
& platform_key
,
49 bool should_parse_media_keys
,
50 base::string16
* error
) {
52 if (platform_key
!= values::kKeybindingPlatformWin
&&
53 platform_key
!= values::kKeybindingPlatformMac
&&
54 platform_key
!= values::kKeybindingPlatformChromeOs
&&
55 platform_key
!= values::kKeybindingPlatformLinux
&&
56 platform_key
!= values::kKeybindingPlatformDefault
) {
57 *error
= ErrorUtils::FormatErrorMessageUTF16(
58 errors::kInvalidKeyBindingUnknownPlatform
,
59 base::IntToString(index
),
61 return ui::Accelerator();
64 std::vector
<std::string
> tokens
;
65 base::SplitString(accelerator
, '+', &tokens
);
66 if (tokens
.size() == 0 ||
67 (tokens
.size() == 1 && DoesRequireModifier(accelerator
)) ||
69 *error
= ErrorUtils::FormatErrorMessageUTF16(
70 errors::kInvalidKeyBinding
,
71 base::IntToString(index
),
74 return ui::Accelerator();
77 // Now, parse it into an accelerator.
78 int modifiers
= ui::EF_NONE
;
79 ui::KeyboardCode key
= ui::VKEY_UNKNOWN
;
80 for (size_t i
= 0; i
< tokens
.size(); i
++) {
81 if (tokens
[i
] == values::kKeyCtrl
) {
82 modifiers
|= ui::EF_CONTROL_DOWN
;
83 } else if (tokens
[i
] == values::kKeyCommand
) {
84 if (platform_key
== values::kKeybindingPlatformMac
) {
85 // Either the developer specified Command+foo in the manifest for Mac or
86 // they specified Ctrl and it got normalized to Command (to get Ctrl on
87 // Mac the developer has to specify MacCtrl). Therefore we treat this
89 modifiers
|= ui::EF_COMMAND_DOWN
;
90 #if defined(OS_MACOSX)
91 } else if (platform_key
== values::kKeybindingPlatformDefault
) {
92 // If we see "Command+foo" in the Default section it can mean two
93 // things, depending on the platform:
94 // The developer specified "Ctrl+foo" for Default and it got normalized
95 // on Mac to "Command+foo". This is fine. Treat it as Command.
96 modifiers
|= ui::EF_COMMAND_DOWN
;
99 // No other platform supports Command.
100 key
= ui::VKEY_UNKNOWN
;
103 } else if (tokens
[i
] == values::kKeyAlt
) {
104 modifiers
|= ui::EF_ALT_DOWN
;
105 } else if (tokens
[i
] == values::kKeyShift
) {
106 modifiers
|= ui::EF_SHIFT_DOWN
;
107 } else if (tokens
[i
].size() == 1 || // A-Z, 0-9.
108 tokens
[i
] == values::kKeyComma
||
109 tokens
[i
] == values::kKeyPeriod
||
110 tokens
[i
] == values::kKeyUp
||
111 tokens
[i
] == values::kKeyDown
||
112 tokens
[i
] == values::kKeyLeft
||
113 tokens
[i
] == values::kKeyRight
||
114 tokens
[i
] == values::kKeyIns
||
115 tokens
[i
] == values::kKeyDel
||
116 tokens
[i
] == values::kKeyHome
||
117 tokens
[i
] == values::kKeyEnd
||
118 tokens
[i
] == values::kKeyPgUp
||
119 tokens
[i
] == values::kKeyPgDwn
||
120 tokens
[i
] == values::kKeyTab
||
121 tokens
[i
] == values::kKeyMediaNextTrack
||
122 tokens
[i
] == values::kKeyMediaPlayPause
||
123 tokens
[i
] == values::kKeyMediaPrevTrack
||
124 tokens
[i
] == values::kKeyMediaStop
) {
125 if (key
!= ui::VKEY_UNKNOWN
) {
126 // Multiple key assignments.
127 key
= ui::VKEY_UNKNOWN
;
131 if (tokens
[i
] == values::kKeyComma
) {
132 key
= ui::VKEY_OEM_COMMA
;
133 } else if (tokens
[i
] == values::kKeyPeriod
) {
134 key
= ui::VKEY_OEM_PERIOD
;
135 } else if (tokens
[i
] == values::kKeyUp
) {
137 } else if (tokens
[i
] == values::kKeyDown
) {
139 } else if (tokens
[i
] == values::kKeyLeft
) {
141 } else if (tokens
[i
] == values::kKeyRight
) {
142 key
= ui::VKEY_RIGHT
;
143 } else if (tokens
[i
] == values::kKeyIns
) {
144 key
= ui::VKEY_INSERT
;
145 } else if (tokens
[i
] == values::kKeyDel
) {
146 key
= ui::VKEY_DELETE
;
147 } else if (tokens
[i
] == values::kKeyHome
) {
149 } else if (tokens
[i
] == values::kKeyEnd
) {
151 } else if (tokens
[i
] == values::kKeyPgUp
) {
152 key
= ui::VKEY_PRIOR
;
153 } else if (tokens
[i
] == values::kKeyPgDwn
) {
155 } else if (tokens
[i
] == values::kKeyTab
) {
157 } else if (tokens
[i
] == values::kKeyMediaNextTrack
&&
158 should_parse_media_keys
) {
159 key
= ui::VKEY_MEDIA_NEXT_TRACK
;
160 } else if (tokens
[i
] == values::kKeyMediaPlayPause
&&
161 should_parse_media_keys
) {
162 key
= ui::VKEY_MEDIA_PLAY_PAUSE
;
163 } else if (tokens
[i
] == values::kKeyMediaPrevTrack
&&
164 should_parse_media_keys
) {
165 key
= ui::VKEY_MEDIA_PREV_TRACK
;
166 } else if (tokens
[i
] == values::kKeyMediaStop
&&
167 should_parse_media_keys
) {
168 key
= ui::VKEY_MEDIA_STOP
;
169 } else if (tokens
[i
].size() == 1 &&
170 tokens
[i
][0] >= 'A' && tokens
[i
][0] <= 'Z') {
171 key
= static_cast<ui::KeyboardCode
>(ui::VKEY_A
+ (tokens
[i
][0] - 'A'));
172 } else if (tokens
[i
].size() == 1 &&
173 tokens
[i
][0] >= '0' && tokens
[i
][0] <= '9') {
174 key
= static_cast<ui::KeyboardCode
>(ui::VKEY_0
+ (tokens
[i
][0] - '0'));
176 key
= ui::VKEY_UNKNOWN
;
180 *error
= ErrorUtils::FormatErrorMessageUTF16(
181 errors::kInvalidKeyBinding
,
182 base::IntToString(index
),
185 return ui::Accelerator();
189 bool command
= (modifiers
& ui::EF_COMMAND_DOWN
) != 0;
190 bool ctrl
= (modifiers
& ui::EF_CONTROL_DOWN
) != 0;
191 bool alt
= (modifiers
& ui::EF_ALT_DOWN
) != 0;
192 bool shift
= (modifiers
& ui::EF_SHIFT_DOWN
) != 0;
194 // We support Ctrl+foo, Alt+foo, Ctrl+Shift+foo, Alt+Shift+foo, but not
195 // Ctrl+Alt+foo and not Shift+foo either. For a more detailed reason why we
196 // don't support Ctrl+Alt+foo see this article:
197 // http://blogs.msdn.com/b/oldnewthing/archive/2004/03/29/101121.aspx.
198 // On Mac Command can also be used in combination with Shift or on its own,
200 if (key
== ui::VKEY_UNKNOWN
|| (ctrl
&& alt
) || (command
&& alt
) ||
201 (shift
&& !ctrl
&& !alt
&& !command
)) {
202 *error
= ErrorUtils::FormatErrorMessageUTF16(
203 errors::kInvalidKeyBinding
,
204 base::IntToString(index
),
207 return ui::Accelerator();
210 if ((key
== ui::VKEY_MEDIA_NEXT_TRACK
||
211 key
== ui::VKEY_MEDIA_PREV_TRACK
||
212 key
== ui::VKEY_MEDIA_PLAY_PAUSE
||
213 key
== ui::VKEY_MEDIA_STOP
) &&
214 (shift
|| ctrl
|| alt
|| command
)) {
215 *error
= ErrorUtils::FormatErrorMessageUTF16(
216 errors::kInvalidKeyBindingMediaKeyWithModifier
,
217 base::IntToString(index
),
220 return ui::Accelerator();
223 return ui::Accelerator(key
, modifiers
);
226 // For Mac, we convert "Ctrl" to "Command" and "MacCtrl" to "Ctrl". Other
227 // platforms leave the shortcut untouched.
228 std::string
NormalizeShortcutSuggestion(const std::string
& suggestion
,
229 const std::string
& platform
) {
230 bool normalize
= false;
231 if (platform
== values::kKeybindingPlatformMac
) {
233 } else if (platform
== values::kKeybindingPlatformDefault
) {
234 #if defined(OS_MACOSX)
242 std::vector
<std::string
> tokens
;
243 base::SplitString(suggestion
, '+', &tokens
);
244 for (size_t i
= 0; i
< tokens
.size(); i
++) {
245 if (tokens
[i
] == values::kKeyCtrl
)
246 tokens
[i
] = values::kKeyCommand
;
247 else if (tokens
[i
] == values::kKeyMacCtrl
)
248 tokens
[i
] = values::kKeyCtrl
;
250 return JoinString(tokens
, '+');
255 Command::Command() : global_(false) {}
257 Command::Command(const std::string
& command_name
,
258 const base::string16
& description
,
259 const std::string
& accelerator
,
261 : command_name_(command_name
),
262 description_(description
),
264 base::string16 error
;
265 accelerator_
= ParseImpl(accelerator
, CommandPlatform(), 0,
266 IsNamedCommand(command_name
), &error
);
269 Command::~Command() {}
272 std::string
Command::CommandPlatform() {
274 return values::kKeybindingPlatformWin
;
275 #elif defined(OS_MACOSX)
276 return values::kKeybindingPlatformMac
;
277 #elif defined(OS_CHROMEOS)
278 return values::kKeybindingPlatformChromeOs
;
279 #elif defined(OS_LINUX)
280 return values::kKeybindingPlatformLinux
;
287 ui::Accelerator
Command::StringToAccelerator(const std::string
& accelerator
,
288 const std::string
& command_name
) {
289 base::string16 error
;
290 ui::Accelerator parsed
=
291 ParseImpl(accelerator
, Command::CommandPlatform(), 0,
292 IsNamedCommand(command_name
), &error
);
297 std::string
Command::AcceleratorToString(const ui::Accelerator
& accelerator
) {
298 std::string shortcut
;
300 // Ctrl and Alt are mutually exclusive.
301 if (accelerator
.IsCtrlDown())
302 shortcut
+= values::kKeyCtrl
;
303 else if (accelerator
.IsAltDown())
304 shortcut
+= values::kKeyAlt
;
305 if (!shortcut
.empty())
306 shortcut
+= values::kKeySeparator
;
308 if (accelerator
.IsCmdDown()) {
309 shortcut
+= values::kKeyCommand
;
310 shortcut
+= values::kKeySeparator
;
313 if (accelerator
.IsShiftDown()) {
314 shortcut
+= values::kKeyShift
;
315 shortcut
+= values::kKeySeparator
;
318 if (accelerator
.key_code() >= ui::VKEY_0
&&
319 accelerator
.key_code() <= ui::VKEY_9
) {
320 shortcut
+= '0' + (accelerator
.key_code() - ui::VKEY_0
);
321 } else if (accelerator
.key_code() >= ui::VKEY_A
&&
322 accelerator
.key_code() <= ui::VKEY_Z
) {
323 shortcut
+= 'A' + (accelerator
.key_code() - ui::VKEY_A
);
325 switch (accelerator
.key_code()) {
326 case ui::VKEY_OEM_COMMA
:
327 shortcut
+= values::kKeyComma
;
329 case ui::VKEY_OEM_PERIOD
:
330 shortcut
+= values::kKeyPeriod
;
333 shortcut
+= values::kKeyUp
;
336 shortcut
+= values::kKeyDown
;
339 shortcut
+= values::kKeyLeft
;
342 shortcut
+= values::kKeyRight
;
344 case ui::VKEY_INSERT
:
345 shortcut
+= values::kKeyIns
;
347 case ui::VKEY_DELETE
:
348 shortcut
+= values::kKeyDel
;
351 shortcut
+= values::kKeyHome
;
354 shortcut
+= values::kKeyEnd
;
357 shortcut
+= values::kKeyPgUp
;
360 shortcut
+= values::kKeyPgDwn
;
363 shortcut
+= values::kKeyTab
;
365 case ui::VKEY_MEDIA_NEXT_TRACK
:
366 shortcut
+= values::kKeyMediaNextTrack
;
368 case ui::VKEY_MEDIA_PLAY_PAUSE
:
369 shortcut
+= values::kKeyMediaPlayPause
;
371 case ui::VKEY_MEDIA_PREV_TRACK
:
372 shortcut
+= values::kKeyMediaPrevTrack
;
374 case ui::VKEY_MEDIA_STOP
:
375 shortcut
+= values::kKeyMediaStop
;
384 bool Command::Parse(const base::DictionaryValue
* command
,
385 const std::string
& command_name
,
387 base::string16
* error
) {
388 DCHECK(!command_name
.empty());
390 base::string16 description
;
391 if (IsNamedCommand(command_name
)) {
392 if (!command
->GetString(keys::kDescription
, &description
) ||
393 description
.empty()) {
394 *error
= ErrorUtils::FormatErrorMessageUTF16(
395 errors::kInvalidKeyBindingDescription
,
396 base::IntToString(index
));
401 // We'll build up a map of platform-to-shortcut suggestions.
402 typedef std::map
<const std::string
, std::string
> SuggestionMap
;
403 SuggestionMap suggestions
;
405 // First try to parse the |suggested_key| as a dictionary.
406 const base::DictionaryValue
* suggested_key_dict
;
407 if (command
->GetDictionary(keys::kSuggestedKey
, &suggested_key_dict
)) {
408 for (base::DictionaryValue::Iterator
iter(*suggested_key_dict
);
409 !iter
.IsAtEnd(); iter
.Advance()) {
410 // For each item in the dictionary, extract the platforms specified.
411 std::string suggested_key_string
;
412 if (iter
.value().GetAsString(&suggested_key_string
) &&
413 !suggested_key_string
.empty()) {
414 // Found a platform, add it to the suggestions list.
415 suggestions
[iter
.key()] = suggested_key_string
;
417 *error
= ErrorUtils::FormatErrorMessageUTF16(
418 errors::kInvalidKeyBinding
,
419 base::IntToString(index
),
426 // No dictionary was found, fall back to using just a string, so developers
427 // don't have to specify a dictionary if they just want to use one default
428 // for all platforms.
429 std::string suggested_key_string
;
430 if (command
->GetString(keys::kSuggestedKey
, &suggested_key_string
) &&
431 !suggested_key_string
.empty()) {
432 // If only a single string is provided, it must be default for all.
433 suggestions
[values::kKeybindingPlatformDefault
] = suggested_key_string
;
435 suggestions
[values::kKeybindingPlatformDefault
] = "";
439 // Check if this is a global or a regular shortcut.
441 if (FeatureSwitch::global_commands()->IsEnabled() &&
442 chrome::VersionInfo::GetChannel() <= chrome::VersionInfo::CHANNEL_DEV
)
443 command
->GetBoolean(keys::kGlobal
, &global
);
445 // Normalize the suggestions.
446 for (SuggestionMap::iterator iter
= suggestions
.begin();
447 iter
!= suggestions
.end(); ++iter
) {
448 // Before we normalize Ctrl to Command we must detect when the developer
449 // specified Command in the Default section, which will work on Mac after
450 // normalization but only fail on other platforms when they try it out on
451 // other platforms, which is not what we want.
452 if (iter
->first
== values::kKeybindingPlatformDefault
&&
453 iter
->second
.find("Command+") != std::string::npos
) {
454 *error
= ErrorUtils::FormatErrorMessageUTF16(
455 errors::kInvalidKeyBinding
,
456 base::IntToString(index
),
458 kCommandKeyNotSupported
);
462 suggestions
[iter
->first
] = NormalizeShortcutSuggestion(iter
->second
,
466 std::string platform
= CommandPlatform();
467 std::string key
= platform
;
468 if (suggestions
.find(key
) == suggestions
.end())
469 key
= values::kKeybindingPlatformDefault
;
470 if (suggestions
.find(key
) == suggestions
.end()) {
471 *error
= ErrorUtils::FormatErrorMessageUTF16(
472 errors::kInvalidKeyBindingMissingPlatform
,
473 base::IntToString(index
),
476 return false; // No platform specified and no fallback. Bail.
479 // For developer convenience, we parse all the suggestions (and complain about
480 // errors for platforms other than the current one) but use only what we need.
481 std::map
<const std::string
, std::string
>::const_iterator iter
=
483 for ( ; iter
!= suggestions
.end(); ++iter
) {
484 ui::Accelerator accelerator
;
485 if (!iter
->second
.empty()) {
486 // Note that we pass iter->first to pretend we are on a platform we're not
488 accelerator
= ParseImpl(iter
->second
, iter
->first
, index
,
489 IsNamedCommand(command_name
), error
);
490 if (accelerator
.key_code() == ui::VKEY_UNKNOWN
) {
491 if (error
->empty()) {
492 *error
= ErrorUtils::FormatErrorMessageUTF16(
493 errors::kInvalidKeyBinding
,
494 base::IntToString(index
),
502 if (iter
->first
== key
) {
503 // This platform is our platform, so grab this key.
504 accelerator_
= accelerator
;
505 command_name_
= command_name
;
506 description_
= description
;
513 base::DictionaryValue
* Command::ToValue(const Extension
* extension
,
515 base::DictionaryValue
* extension_data
= new base::DictionaryValue();
517 base::string16 command_description
;
518 bool extension_action
= false;
519 if (command_name() == values::kBrowserActionCommandEvent
||
520 command_name() == values::kPageActionCommandEvent
||
521 command_name() == values::kScriptBadgeCommandEvent
) {
522 command_description
=
523 l10n_util::GetStringUTF16(IDS_EXTENSION_COMMANDS_GENERIC_ACTIVATE
);
524 extension_action
= true;
526 command_description
= description();
528 extension_data
->SetString("description", command_description
);
529 extension_data
->SetBoolean("active", active
);
530 extension_data
->SetString("keybinding", accelerator().GetShortcutText());
531 extension_data
->SetString("command_name", command_name());
532 extension_data
->SetString("extension_id", extension
->id());
533 extension_data
->SetBoolean("global", global());
534 extension_data
->SetBoolean("extension_action", extension_action
);
536 if (FeatureSwitch::global_commands()->IsEnabled()) {
537 // TODO(finnur): This is to make sure we don't show the config UI beyond
538 // dev and will be removed when we launch.
539 static bool stable_or_beta
=
540 chrome::VersionInfo::GetChannel() >= chrome::VersionInfo::CHANNEL_BETA
;
541 extension_data
->SetBoolean("scope_ui_visible", !stable_or_beta
);
544 return extension_data
;
547 } // namespace extensions