Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(248)

Side by Side Diff: chrome/browser/autocomplete/keyword_provider.cc

Issue 6731036: Enabled pressing TAB to cycle through the Omnibox results. (Closed) Base URL: http://src.chromium.org/svn/trunk/src/
Patch Set: '' Created 8 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "chrome/browser/autocomplete/keyword_provider.h" 5 #include "chrome/browser/autocomplete/keyword_provider.h"
6 6
7 #include <algorithm> 7 #include <algorithm>
8 #include <vector> 8 #include <vector>
9 9
10 #include "base/string16.h" 10 #include "base/string16.h"
(...skipping 24 matching lines...) Expand all
35 provider_->MaybeEndExtensionKeywordMode(); 35 provider_->MaybeEndExtensionKeywordMode();
36 } 36 }
37 37
38 void StayInKeywordMode() { 38 void StayInKeywordMode() {
39 provider_ = NULL; 39 provider_ = NULL;
40 } 40 }
41 private: 41 private:
42 KeywordProvider* provider_; 42 KeywordProvider* provider_;
43 }; 43 };
44 44
45 // static
46 string16 KeywordProvider::SplitReplacementStringFromInput(
47 const string16& input,
48 bool trim_leading_whitespace) {
49 // The input may contain leading whitespace, strip it.
50 string16 trimmed_input;
51 TrimWhitespace(input, TRIM_LEADING, &trimmed_input);
52
53 // And extract the replacement string.
54 string16 remaining_input;
55 SplitKeywordFromInput(trimmed_input, trim_leading_whitespace,
56 &remaining_input);
57 return remaining_input;
58 }
59
60 KeywordProvider::KeywordProvider(ACProviderListener* listener, Profile* profile) 45 KeywordProvider::KeywordProvider(ACProviderListener* listener, Profile* profile)
61 : AutocompleteProvider(listener, profile, "Keyword"), 46 : AutocompleteProvider(listener, profile, "Keyword"),
62 model_(NULL), 47 model_(NULL),
63 current_input_id_(0) { 48 current_input_id_(0) {
64 // Extension suggestions always come from the original profile, since that's 49 // Extension suggestions always come from the original profile, since that's
65 // where extensions run. We use the input ID to distinguish whether the 50 // where extensions run. We use the input ID to distinguish whether the
66 // suggestions are meant for us. 51 // suggestions are meant for us.
67 registrar_.Add(this, 52 registrar_.Add(this,
68 chrome::NOTIFICATION_EXTENSION_OMNIBOX_SUGGESTIONS_READY, 53 chrome::NOTIFICATION_EXTENSION_OMNIBOX_SUGGESTIONS_READY,
69 content::Source<Profile>(profile->GetOriginalProfile())); 54 content::Source<Profile>(profile->GetOriginalProfile()));
70 registrar_.Add( 55 registrar_.Add(
71 this, chrome::NOTIFICATION_EXTENSION_OMNIBOX_DEFAULT_SUGGESTION_CHANGED, 56 this, chrome::NOTIFICATION_EXTENSION_OMNIBOX_DEFAULT_SUGGESTION_CHANGED,
72 content::Source<Profile>(profile->GetOriginalProfile())); 57 content::Source<Profile>(profile->GetOriginalProfile()));
73 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_OMNIBOX_INPUT_ENTERED, 58 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_OMNIBOX_INPUT_ENTERED,
74 content::Source<Profile>(profile)); 59 content::Source<Profile>(profile));
60
61 // Start loading the keyword DB now so it is ready by the time
62 // the user starts typing.
63 GetTemplateURLService();
Peter Kasting 2012/01/11 03:00:16 I'm worried about doing this. This can slow start
75 } 64 }
76 65
77 KeywordProvider::KeywordProvider(ACProviderListener* listener, 66 KeywordProvider::KeywordProvider(ACProviderListener* listener,
78 TemplateURLService* model) 67 TemplateURLService* model)
79 : AutocompleteProvider(listener, NULL, "Keyword"), 68 : AutocompleteProvider(listener, NULL, "Keyword"),
80 model_(model), 69 model_(model),
81 current_input_id_(0) { 70 current_input_id_(0) {
82 } 71 }
83 72
84 73
(...skipping 15 matching lines...) Expand all
100 } 89 }
101 }; 90 };
102 91
103 // We need our input IDs to be unique across all profiles, so we keep a global 92 // We need our input IDs to be unique across all profiles, so we keep a global
104 // UID that each provider uses. 93 // UID that each provider uses.
105 static int global_input_uid_; 94 static int global_input_uid_;
106 95
107 } // namespace 96 } // namespace
108 97
109 // static 98 // static
99 string16 KeywordProvider::SplitKeywordFromInput(
100 const string16& input,
101 bool trim_leading_whitespace,
102 string16* remaining_input) {
103 // Find end of first token. The AutocompleteController has trimmed leading
104 // whitespace, so we need not skip over that.
105 const size_t first_white(input.find_first_of(kWhitespaceUTF16));
106 DCHECK_NE(0U, first_white);
107 if (first_white == string16::npos)
108 return input; // Only one token provided.
109
110 // Set |remaining_input| to everything after the first token.
111 DCHECK(remaining_input != NULL);
112 const size_t remaining_start = trim_leading_whitespace ?
113 input.find_first_not_of(kWhitespaceUTF16, first_white) : first_white + 1;
114
115 if (remaining_start < input.length())
116 remaining_input->assign(input.begin() + remaining_start, input.end());
117
118 // Return first token as keyword.
119 return input.substr(0, first_white);
120 }
121
122 // static
123 string16 KeywordProvider::SplitReplacementStringFromInput(
124 const string16& input,
125 bool trim_leading_whitespace) {
126 // The input may contain leading whitespace, strip it.
127 string16 trimmed_input;
128 TrimWhitespace(input, TRIM_LEADING, &trimmed_input);
129
130 // And extract the replacement string.
131 string16 remaining_input;
132 SplitKeywordFromInput(trimmed_input, trim_leading_whitespace,
133 &remaining_input);
134 return remaining_input;
135 }
136
137 // static
110 const TemplateURL* KeywordProvider::GetSubstitutingTemplateURLForInput( 138 const TemplateURL* KeywordProvider::GetSubstitutingTemplateURLForInput(
111 Profile* profile, 139 Profile* profile,
112 const AutocompleteInput& input, 140 const AutocompleteInput& input,
113 string16* remaining_input) { 141 string16* remaining_input) {
114 if (!input.allow_exact_keyword_match()) 142 if (!input.allow_exact_keyword_match())
115 return NULL; 143 return NULL;
116 144
117 string16 keyword; 145 string16 keyword;
118 if (!ExtractKeywordFromInput(input, &keyword, remaining_input)) 146 if (!ExtractKeywordFromInput(input, &keyword, remaining_input))
119 return NULL; 147 return NULL;
120 148
121 // Make sure the model is loaded. This is cheap and quickly bails out if 149 // Make sure the model is loaded. This is cheap and quickly bails out if
122 // the model is already loaded. 150 // the model is already loaded.
123 TemplateURLService* model = TemplateURLServiceFactory::GetForProfile(profile); 151 TemplateURLService* model = TemplateURLServiceFactory::GetForProfile(profile);
124 DCHECK(model); 152 DCHECK(model);
125 model->Load(); 153 model->Load();
126 154
127 const TemplateURL* template_url = model->GetTemplateURLForKeyword(keyword); 155 const TemplateURL* template_url = model->GetTemplateURLForKeyword(keyword);
128 return TemplateURL::SupportsReplacement(template_url) ? template_url : NULL; 156 return TemplateURL::SupportsReplacement(template_url) ? template_url : NULL;
129 } 157 }
130 158
159 string16 KeywordProvider::GetKeywordForText(
160 const string16& text) const {
161 string16 remaining_input;
Peter Kasting 2012/01/11 03:00:16 Nit: This variable is dead.
162 const string16 keyword(TemplateURLService::CleanUserInputKeyword(text));
163
164 if (keyword.empty())
165 return keyword;
166
167 TemplateURLService* url_service = GetTemplateURLService();
168 if (!url_service)
169 return string16();
170
171 // Don't provide a keyword if it doesn't support replacement.
172 const TemplateURL* const template_url =
173 url_service->GetTemplateURLForKeyword(keyword);
174 if (!TemplateURL::SupportsReplacement(template_url))
175 return string16();
176
177 // Don't provide a keyword for inactive/disabled extension keywords.
178 if (template_url->IsExtensionKeyword()) {
179 const Extension* extension = profile_->GetExtensionService()->
180 GetExtensionById(template_url->GetExtensionId(), false);
181 if (!extension ||
182 (profile_->IsOffTheRecord() &&
183 !profile_->GetExtensionService()->IsIncognitoEnabled(extension->id())))
184 return string16();
185 }
186
187 return keyword;
188 }
189
190 AutocompleteMatch KeywordProvider::CreateAutocompleteMatch(
191 const string16& text,
192 const string16& keyword,
193 const AutocompleteInput& input) {
194 return CreateAutocompleteMatch(GetTemplateURLService(), keyword, input,
195 keyword.size(), SplitReplacementStringFromInput(text, true), 0);
196 }
197
131 void KeywordProvider::Start(const AutocompleteInput& input, 198 void KeywordProvider::Start(const AutocompleteInput& input,
132 bool minimal_changes) { 199 bool minimal_changes) {
133 // This object ensures we end keyword mode if we exit the function without 200 // This object ensures we end keyword mode if we exit the function without
134 // toggling keyword mode to on. 201 // toggling keyword mode to on.
135 ScopedEndExtensionKeywordMode keyword_mode_toggle(this); 202 ScopedEndExtensionKeywordMode keyword_mode_toggle(this);
136 203
137 matches_.clear(); 204 matches_.clear();
138 205
139 if (!minimal_changes) { 206 if (!minimal_changes) {
140 done_ = true; 207 done_ = true;
(...skipping 13 matching lines...) Expand all
154 // whatever we do here! 221 // whatever we do here!
155 // 222 //
156 // TODO(pkasting): http://b/1112681 If someday we remember usage frequency for 223 // TODO(pkasting): http://b/1112681 If someday we remember usage frequency for
157 // keywords, we might suggest keywords that haven't even been partially typed, 224 // keywords, we might suggest keywords that haven't even been partially typed,
158 // if the user uses them enough and isn't obviously typing something else. In 225 // if the user uses them enough and isn't obviously typing something else. In
159 // this case we'd consider all input here to be query input. 226 // this case we'd consider all input here to be query input.
160 string16 keyword, remaining_input; 227 string16 keyword, remaining_input;
161 if (!ExtractKeywordFromInput(input, &keyword, &remaining_input)) 228 if (!ExtractKeywordFromInput(input, &keyword, &remaining_input))
162 return; 229 return;
163 230
164 // Make sure the model is loaded. This is cheap and quickly bails out if 231 TemplateURLService* model = GetTemplateURLService();
165 // the model is already loaded.
166 TemplateURLService* model =
167 profile_ ?
168 TemplateURLServiceFactory::GetForProfile(profile_) :
169 model_;
170 DCHECK(model);
171 model->Load();
172 232
173 // Get the best matches for this keyword. 233 // Get the best matches for this keyword.
174 // 234 //
175 // NOTE: We could cache the previous keywords and reuse them here in the 235 // NOTE: We could cache the previous keywords and reuse them here in the
176 // |minimal_changes| case, but since we'd still have to recalculate their 236 // |minimal_changes| case, but since we'd still have to recalculate their
177 // relevances and we can just recreate the results synchronously anyway, we 237 // relevances and we can just recreate the results synchronously anyway, we
178 // don't bother. 238 // don't bother.
179 // 239 //
180 // TODO(pkasting): http://b/893701 We should remember the user's use of a 240 // TODO(pkasting): http://b/893701 We should remember the user's use of a
181 // search query both from the autocomplete popup and from web pages 241 // search query both from the autocomplete popup and from web pages
182 // themselves. 242 // themselves.
183 std::vector<string16> keyword_matches; 243 std::vector<string16> keyword_matches;
184 model->FindMatchingKeywords(keyword, 244 model->FindMatchingKeywords(keyword,
185 !remaining_input.empty(), 245 !remaining_input.empty(),
186 &keyword_matches); 246 &keyword_matches);
187 247
188 // Prune any extension keywords that are disallowed in incognito mode (if
189 // we're incognito), or disabled.
190 for (std::vector<string16>::iterator i(keyword_matches.begin()); 248 for (std::vector<string16>::iterator i(keyword_matches.begin());
191 i != keyword_matches.end(); ) { 249 i != keyword_matches.end(); ) {
192 const TemplateURL* template_url(model->GetTemplateURLForKeyword(*i)); 250 const TemplateURL* template_url(model->GetTemplateURLForKeyword(*i));
251
252 // Prune any extension keywords that are disallowed in incognito mode (if
253 // we're incognito), or disabled.
193 if (profile_ && 254 if (profile_ &&
194 input.matches_requested() == AutocompleteInput::ALL_MATCHES && 255 input.matches_requested() == AutocompleteInput::ALL_MATCHES &&
195 template_url->IsExtensionKeyword()) { 256 template_url->IsExtensionKeyword()) {
196 ExtensionService* service = profile_->GetExtensionService(); 257 ExtensionService* service = profile_->GetExtensionService();
197 const Extension* extension = service->GetExtensionById( 258 const Extension* extension = service->GetExtensionById(
198 template_url->GetExtensionId(), false); 259 template_url->GetExtensionId(), false);
199 bool enabled = 260 bool enabled =
200 extension && (!profile_->IsOffTheRecord() || 261 extension && (!profile_->IsOffTheRecord() ||
201 service->IsIncognitoEnabled(extension->id())); 262 service->IsIncognitoEnabled(extension->id()));
202 if (!enabled) { 263 if (!enabled) {
203 i = keyword_matches.erase(i); 264 i = keyword_matches.erase(i);
204 continue; 265 continue;
205 } 266 }
206 } 267 }
268
269 // Prune any substituting keywords if there is no substitution.
270 if (TemplateURL::SupportsReplacement(template_url) &&
271 !input.allow_exact_keyword_match()) {
272 i = keyword_matches.erase(i);
273 continue;
274 }
275
207 ++i; 276 ++i;
208 } 277 }
209 if (keyword_matches.empty()) 278 if (keyword_matches.empty())
210 return; 279 return;
211 std::sort(keyword_matches.begin(), keyword_matches.end(), CompareQuality()); 280 std::sort(keyword_matches.begin(), keyword_matches.end(), CompareQuality());
212 281
213 // Limit to one exact or three inexact matches, and mark them up for display 282 // Limit to one exact or three inexact matches, and mark them up for display
214 // in the autocomplete popup. 283 // in the autocomplete popup.
215 // Any exact match is going to be the highest quality match, and thus at the 284 // Any exact match is going to be the highest quality match, and thus at the
216 // front of our vector. 285 // front of our vector.
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after
287 return false; 356 return false;
288 357
289 string16 trimmed_input; 358 string16 trimmed_input;
290 TrimWhitespace(input.text(), TRIM_TRAILING, &trimmed_input); 359 TrimWhitespace(input.text(), TRIM_TRAILING, &trimmed_input);
291 *keyword = TemplateURLService::CleanUserInputKeyword( 360 *keyword = TemplateURLService::CleanUserInputKeyword(
292 SplitKeywordFromInput(trimmed_input, true, remaining_input)); 361 SplitKeywordFromInput(trimmed_input, true, remaining_input));
293 return !keyword->empty(); 362 return !keyword->empty();
294 } 363 }
295 364
296 // static 365 // static
297 string16 KeywordProvider::SplitKeywordFromInput(
298 const string16& input,
299 bool trim_leading_whitespace,
300 string16* remaining_input) {
301 // Find end of first token. The AutocompleteController has trimmed leading
302 // whitespace, so we need not skip over that.
303 const size_t first_white(input.find_first_of(kWhitespaceUTF16));
304 DCHECK_NE(0U, first_white);
305 if (first_white == string16::npos)
306 return input; // Only one token provided.
307
308 // Set |remaining_input| to everything after the first token.
309 DCHECK(remaining_input != NULL);
310 const size_t remaining_start = trim_leading_whitespace ?
311 input.find_first_not_of(kWhitespaceUTF16, first_white) : first_white + 1;
312
313 if (remaining_start < input.length())
314 remaining_input->assign(input.begin() + remaining_start, input.end());
315
316 // Return first token as keyword.
317 return input.substr(0, first_white);
318 }
319
320 // static
321 void KeywordProvider::FillInURLAndContents( 366 void KeywordProvider::FillInURLAndContents(
322 Profile* profile, 367 Profile* profile,
323 const string16& remaining_input, 368 const string16& remaining_input,
324 const TemplateURL* element, 369 const TemplateURL* element,
325 AutocompleteMatch* match) { 370 AutocompleteMatch* match) {
326 DCHECK(!element->short_name().empty()); 371 DCHECK(!element->short_name().empty());
327 DCHECK(element->url()); 372 DCHECK(element->url());
328 DCHECK(element->url()->IsValid()); 373 DCHECK(element->url()->IsValid());
329 int message_id = element->IsExtensionKeyword() ? 374 int message_id = element->IsExtensionKeyword() ?
330 IDS_EXTENSION_KEYWORD_COMMAND : IDS_KEYWORD_SEARCH; 375 IDS_EXTENSION_KEYWORD_COMMAND : IDS_KEYWORD_SEARCH;
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after
408 const bool keyword_complete = (prefix_length == keyword.length()); 453 const bool keyword_complete = (prefix_length == keyword.length());
409 if (relevance < 0) { 454 if (relevance < 0) {
410 relevance = 455 relevance =
411 CalculateRelevance(input.type(), keyword_complete, 456 CalculateRelevance(input.type(), keyword_complete,
412 // When the user wants keyword matches to take 457 // When the user wants keyword matches to take
413 // preference, score them highly regardless of 458 // preference, score them highly regardless of
414 // whether the input provides query text. 459 // whether the input provides query text.
415 supports_replacement, input.prefer_keyword(), 460 supports_replacement, input.prefer_keyword(),
416 input.allow_exact_keyword_match()); 461 input.allow_exact_keyword_match());
417 } 462 }
418 AutocompleteMatch result(this, relevance, false, 463 AutocompleteMatch match(this, relevance, false,
419 supports_replacement ? AutocompleteMatch::SEARCH_OTHER_ENGINE : 464 supports_replacement ? AutocompleteMatch::SEARCH_OTHER_ENGINE :
420 AutocompleteMatch::HISTORY_KEYWORD); 465 AutocompleteMatch::HISTORY_KEYWORD);
421 result.fill_into_edit.assign(keyword); 466 match.fill_into_edit.assign(keyword);
422 if (!remaining_input.empty() || !keyword_complete || supports_replacement) 467 if (!remaining_input.empty() || !keyword_complete || supports_replacement)
423 result.fill_into_edit.push_back(L' '); 468 match.fill_into_edit.push_back(L' ');
424 result.fill_into_edit.append(remaining_input); 469 match.fill_into_edit.append(remaining_input);
425 // If we wanted to set |result.inline_autocomplete_offset| correctly, we'd 470 // If we wanted to set |result.inline_autocomplete_offset| correctly, we'd
426 // need CleanUserInputKeyword() to return the amount of adjustment it's made 471 // need CleanUserInputKeyword() to return the amount of adjustment it's made
427 // to the user's input. Because right now inexact keyword matches can't score 472 // to the user's input. Because right now inexact keyword matches can't score
428 // more highly than a "what you typed" match from one of the other providers, 473 // more highly than a "what you typed" match from one of the other providers,
429 // we just don't bother to do this, and leave inline autocompletion off. 474 // we just don't bother to do this, and leave inline autocompletion off.
430 result.inline_autocomplete_offset = string16::npos; 475 match.inline_autocomplete_offset = string16::npos;
431 476
432 // Create destination URL and popup entry content by substituting user input 477 // Create destination URL and popup entry content by substituting user input
433 // into keyword templates. 478 // into keyword templates.
434 FillInURLAndContents(profile_, remaining_input, element, &result); 479 FillInURLAndContents(profile_, remaining_input, element, &match);
435 480
436 if (supports_replacement) 481 if (supports_replacement)
437 result.template_url = element; 482 match.template_url = element;
438 result.transition = content::PAGE_TRANSITION_KEYWORD; 483 match.keyword = keyword;
484 match.transition = content::PAGE_TRANSITION_KEYWORD;
439 485
440 return result; 486 return match;
441 } 487 }
442 488
443 void KeywordProvider::Observe(int type, 489 void KeywordProvider::Observe(int type,
444 const content::NotificationSource& source, 490 const content::NotificationSource& source,
445 const content::NotificationDetails& details) { 491 const content::NotificationDetails& details) {
446 TemplateURLService* model = 492 TemplateURLService* model = GetTemplateURLService();
447 profile_ ? TemplateURLServiceFactory::GetForProfile(profile_) : model_;
448 const AutocompleteInput& input = extension_suggest_last_input_; 493 const AutocompleteInput& input = extension_suggest_last_input_;
449 494
450 switch (type) { 495 switch (type) {
451 case chrome::NOTIFICATION_EXTENSION_OMNIBOX_INPUT_ENTERED: 496 case chrome::NOTIFICATION_EXTENSION_OMNIBOX_INPUT_ENTERED:
452 // Input has been accepted, so we're done with this input session. Ensure 497 // Input has been accepted, so we're done with this input session. Ensure
453 // we don't send the OnInputCancelled event, or handle any more stray 498 // we don't send the OnInputCancelled event, or handle any more stray
454 // suggestions_ready events. 499 // suggestions_ready events.
455 current_keyword_extension_id_.clear(); 500 current_keyword_extension_id_.clear();
456 current_input_id_ = 0; 501 current_input_id_ = 0;
457 return; 502 return;
(...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after
514 listener_->OnProviderUpdate(!extension_suggest_matches_.empty()); 559 listener_->OnProviderUpdate(!extension_suggest_matches_.empty());
515 return; 560 return;
516 } 561 }
517 562
518 default: 563 default:
519 NOTREACHED(); 564 NOTREACHED();
520 return; 565 return;
521 } 566 }
522 } 567 }
523 568
569 TemplateURLService* KeywordProvider::GetTemplateURLService() const {
570 TemplateURLService* service = profile_ ?
571 TemplateURLServiceFactory::GetForProfile(profile_) : model_;
572 // Make sure the model is loaded. This is cheap and quickly bails out if
573 // the model is already loaded.
574 DCHECK(service);
575 service->Load();
576 return service;
577 }
578
524 void KeywordProvider::EnterExtensionKeywordMode( 579 void KeywordProvider::EnterExtensionKeywordMode(
525 const std::string& extension_id) { 580 const std::string& extension_id) {
526 DCHECK(current_keyword_extension_id_.empty()); 581 DCHECK(current_keyword_extension_id_.empty());
527 current_keyword_extension_id_ = extension_id; 582 current_keyword_extension_id_ = extension_id;
528 583
529 ExtensionOmniboxEventRouter::OnInputStarted( 584 ExtensionOmniboxEventRouter::OnInputStarted(
530 profile_, current_keyword_extension_id_); 585 profile_, current_keyword_extension_id_);
531 } 586 }
532 587
533 void KeywordProvider::MaybeEndExtensionKeywordMode() { 588 void KeywordProvider::MaybeEndExtensionKeywordMode() {
534 if (!current_keyword_extension_id_.empty()) { 589 if (!current_keyword_extension_id_.empty()) {
535 ExtensionOmniboxEventRouter::OnInputCancelled( 590 ExtensionOmniboxEventRouter::OnInputCancelled(
536 profile_, current_keyword_extension_id_); 591 profile_, current_keyword_extension_id_);
537 592
538 current_keyword_extension_id_.clear(); 593 current_keyword_extension_id_.clear();
539 } 594 }
540 } 595 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698