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

Side by Side Diff: chrome/browser/bookmarks/bookmark_tag_model.cc

Issue 26894002: Experimental bookmark model based on tags. (Closed) Base URL: http://git.chromium.org/chromium/src.git@master
Patch Set: Fixing trybot failures. Created 7 years, 2 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
OLDNEW
(Empty)
1 // Copyright 2013 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.
4
5 #include "chrome/browser/bookmarks/bookmark_tag_model.h"
6
7 #include "base/auto_reset.h"
8 #include "base/json/json_string_value_serializer.h"
9 #include "base/observer_list.h"
10 #include "base/strings/string_util.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "chrome/browser/bookmarks/bookmark_model_factory.h"
13 #include "chrome/browser/bookmarks/bookmark_tag_model_observer.h"
14 #include "ui/base/models/tree_node_iterator.h"
15
16 namespace {
17 // The key used to store the tag list in the metainfo of a bookmark.
18 const char* TAG_KEY = "TAG_KEY";
19
20 // Comparator to sort tags by usage.
21 struct TagComparator {
22 TagComparator(std::map<BookmarkTag, unsigned int>& tags) : tags_(tags) {
23 }
24 ~TagComparator() {}
25
26 bool operator()(const BookmarkTag& a, const BookmarkTag& b) {
27 return (tags_[a] < tags_[b]);
28 }
29
30 std::map<BookmarkTag, unsigned int>& tags_;
31 };
32
33 // The tags are currently stored in the BookmarkNode's metaInfo in JSON
34 // format. This function extracts the info from there and returns it in
35 // digestible format.
36 // If the Bookmark was never tagged before it is implicitely tagged with the
37 // title of all its ancestors in the BookmarkModel.
38 std::set<BookmarkTag> ExtractTagsFromBookmark(
39 const BookmarkNode* bookmark) {
sky 2013/10/16 13:46:19 nit: seems like you can fit all on one line.
noyau (Ping after 24h) 2013/10/16 16:51:32 Done.
40 // This is awful BTW. Metainfo is itself an encoded JSON, and here we decode
41 // another layer.
42
43 // Retrieve the encodedData from the bookmark. If there is no encoded data
44 // at all returns the name of all the ancestors as separate tags.
45 std::string encoded;
46 if (!bookmark->GetMetaInfo(TAG_KEY, &encoded)) {
47 std::set<BookmarkTag> tags;
48 const BookmarkNode* folder = bookmark->parent();
49 while (folder && folder->type() == BookmarkNode::FOLDER) {
50 BookmarkTag trimmed_tag = CollapseWhitespace(folder->GetTitle(), true);
51 if (!trimmed_tag.empty())
52 tags.insert(trimmed_tag);
53 folder = folder->parent();
54 }
55 return tags;
56 }
57
58 // Decode into a base::Value. If the data is not encoded properly as a list
59 // return an empty result.
60 JSONStringValueSerializer serializer(&encoded);
61 int error_code = 0;
62 std::string error_message;
63 scoped_ptr<base::Value> result(serializer.Deserialize(&error_code,
64 &error_message));
65
66 if (error_code || !result->IsType(base::Value::TYPE_LIST))
67 return std::set<BookmarkTag>();
68
69 base::ListValue* list = NULL;
70 if (!result->GetAsList(&list) || list->empty())
71 return std::set<BookmarkTag>();
72
73 // Build the set.
74 std::set<BookmarkTag> return_value;
75
76 for (base::ListValue::iterator it = list->begin();
77 it != list->end(); ++it) {
78 base::Value* item = *it;
79 BookmarkTag tag;
80 if (!item->GetAsString(&tag))
81 continue;
82 return_value.insert(tag);
83 }
84 return return_value;
85 }
86 } // namespace
87
88 BookmarkTagModel::BookmarkTagModel(BookmarkModel* bookmark_model)
89 : bookmark_model_(bookmark_model),
90 loaded_(false),
91 observers_(ObserverList<BookmarkTagModelObserver>::NOTIFY_EXISTING_ONLY),
92 inhibit_change_notifications_(false) {
93 bookmark_model_->AddObserver(this);
94 if (bookmark_model_->loaded())
95 Load();
96 }
97
98 BookmarkTagModel::~BookmarkTagModel() {
99 if (bookmark_model_)
100 bookmark_model_->RemoveObserver(this);
101 }
102
103 // BookmarkModel forwarding.
104
105 void BookmarkTagModel::AddObserver(BookmarkTagModelObserver* observer) {
106 observers_.AddObserver(observer);
107 }
108
109 void BookmarkTagModel::RemoveObserver(BookmarkTagModelObserver* observer) {
110 observers_.RemoveObserver(observer);
111 }
112
113 void BookmarkTagModel::BeginExtensiveChanges() {
114 DCHECK(bookmark_model_);
115 bookmark_model_->BeginExtensiveChanges();
116 }
117
118 void BookmarkTagModel::EndExtensiveChanges() {
119 DCHECK(bookmark_model_);
120 bookmark_model_->EndExtensiveChanges();
121 }
122
123 bool BookmarkTagModel::IsDoingExtensiveChanges() const {
124 DCHECK(bookmark_model_);
125 return bookmark_model_->IsDoingExtensiveChanges();
126 }
127
128 void BookmarkTagModel::Remove(const BookmarkNode* bookmark) {
129 DCHECK(bookmark_model_);
130 DCHECK(loaded_);
131 const BookmarkNode* parent = bookmark->parent();
132 bookmark_model_->Remove(parent, parent->GetIndexOf(bookmark));
133 }
134
135 void BookmarkTagModel::RemoveAll() {
136 DCHECK(bookmark_model_);
137 DCHECK(loaded_);
138 bookmark_model_->RemoveAll();
139 }
140
141 const gfx::Image& BookmarkTagModel::GetFavicon(const BookmarkNode* bookmark) {
142 DCHECK(bookmark_model_);
143 DCHECK(loaded_);
144 return bookmark_model_->GetFavicon(bookmark);
145 }
146
147 void BookmarkTagModel::SetTitle(const BookmarkNode* bookmark,
148 const string16& title) {
149 DCHECK(bookmark_model_);
150 DCHECK(loaded_);
151 bookmark_model_->SetTitle(bookmark, title);
152 }
153
154 void BookmarkTagModel::SetURL(const BookmarkNode* bookmark, const GURL& url) {
155 DCHECK(bookmark_model_);
156 DCHECK(loaded_);
157 bookmark_model_->SetURL(bookmark, url);
158 }
159
160 void BookmarkTagModel::SetDateAdded(const BookmarkNode* bookmark,
161 base::Time date_added) {
162 DCHECK(bookmark_model_);
163 DCHECK(loaded_);
164 bookmark_model_->SetDateAdded(bookmark, date_added);
165 }
166
167 const BookmarkNode*
168 BookmarkTagModel::GetMostRecentlyAddedBookmarkForURL(const GURL& url) {
169 DCHECK(bookmark_model_);
170 DCHECK(loaded_);
171 return bookmark_model_->GetMostRecentlyAddedNodeForURL(url);
172 }
173
174 // Tags specific code.
175
176 const BookmarkNode* BookmarkTagModel::AddURL(
177 const string16& title,
178 const GURL& url,
179 const std::set<BookmarkTag>& tags) {
180 DCHECK(bookmark_model_);
181 DCHECK(loaded_);
182
183 const BookmarkNode* bookmark;
184 {
185 base::AutoReset<bool> inhibitor(&inhibit_change_notifications_, true);
186 const BookmarkNode* parent = bookmark_model_->GetParentForNewNodes();
187 bookmark = bookmark_model_->AddURL(parent, 0, title, url);
188 AddTagsToBookmark(tags, bookmark);
189 }
190 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
191 BookmarkNodeAdded(this, bookmark));
192
193 return bookmark;
194 }
195
196 std::set<BookmarkTag> BookmarkTagModel::GetTagsForBookmark(
197 const BookmarkNode* bookmark) {
198 DCHECK(loaded_);
199 return bookmark_to_tags_[bookmark];
200 }
201
202 void BookmarkTagModel::AddTagsToBookmark(
203 const std::set<BookmarkTag>& tags,
204 const BookmarkNode* bookmark) {
205 DCHECK(bookmark_model_);
206 std::set<BookmarkTag> all_tags(GetTagsForBookmark(bookmark));
207 for (std::set<BookmarkTag>::const_iterator it = tags.begin();
208 it != tags.end(); ++it) {
209 BookmarkTag trimmed_tag = CollapseWhitespace(*it, true);
210 if (trimmed_tag.empty())
211 continue;
212 all_tags.insert(trimmed_tag);
213 }
214 SetTagsOnBookmark(all_tags, bookmark);
215 }
216
217 void BookmarkTagModel::AddTagsToBookmarks(
218 const std::set<BookmarkTag>& tags,
219 const std::set<const BookmarkNode*>& bookmarks) {
220 for (std::set<const BookmarkNode*>::const_iterator it = bookmarks.begin();
221 it != bookmarks.end(); ++it) {
222 AddTagsToBookmark(tags, *it);
223 }
224 }
225
226 void BookmarkTagModel::RemoveTagsFromBookmark(
227 const std::set<BookmarkTag>& tags,
228 const BookmarkNode* bookmark) {
229 std::set<BookmarkTag> all_tags(GetTagsForBookmark(bookmark));
230 for (std::set<BookmarkTag>::const_iterator it = tags.begin();
231 it != tags.end(); ++it) {
232 all_tags.erase(*it);
233 }
234 SetTagsOnBookmark(all_tags, bookmark);
235 }
236
237 void BookmarkTagModel::RemoveTagsFromBookmarks(
238 const std::set<BookmarkTag>& tags,
239 const std::set<const BookmarkNode*>& bookmarks){
240 for (std::set<const BookmarkNode*>::const_iterator it = bookmarks.begin();
241 it != bookmarks.end(); ++it) {
242 RemoveTagsFromBookmark(tags, *it);
243 }
244 }
245
246 std::set<const BookmarkNode*> BookmarkTagModel::BookmarksForTags(
247 const std::set<BookmarkTag>& tags) {
248 DCHECK(loaded_);
249 // Count for each tags how many times a bookmark appeared.
250 std::map<const BookmarkNode*, size_t> bookmark_counts;
251 for (std::set<BookmarkTag>::const_iterator it = tags.begin();
252 it != tags.end(); ++it) {
253 const std::set<const BookmarkNode*>& subset(tag_to_bookmarks_[*it]);
254 for (std::set<const BookmarkNode*>::const_iterator tag_it = subset.begin();
255 tag_it != subset.end(); ++tag_it) {
256 bookmark_counts[*tag_it] += 1;
257 }
258 }
259 // Keep only the bookmarks that appeared in all the tags.
260 std::set<const BookmarkNode*> common_bookmarks;
261 for (std::map<const BookmarkNode*, size_t>::iterator it =
262 bookmark_counts.begin(); it != bookmark_counts.end(); ++it) {
263 if (it->second == tags.size())
264 common_bookmarks.insert(it->first);
265 }
266 return common_bookmarks;
267 }
268
269 std::set<const BookmarkNode*> BookmarkTagModel::BookmarksForTag(
270 const BookmarkTag& tag) {
271 DCHECK(!tag.empty());
272 return tag_to_bookmarks_[tag];
273 }
274
275 std::vector<BookmarkTag> BookmarkTagModel::TagsRelatedToTag(
276 const BookmarkTag& tag) {
277 DCHECK(loaded_);
278 std::map<BookmarkTag, unsigned int> tags;
279
280 if (tag.empty()) {
281 // Returns all the tags.
282 for (std::map<const BookmarkTag, std::set<const BookmarkNode*> >::iterator
283 it = tag_to_bookmarks_.begin(); it != tag_to_bookmarks_.end(); ++it) {
284 tags[it->first] = it->second.size();
285 }
286 } else {
287 std::set<const BookmarkNode*> bookmarks(BookmarksForTag(tag));
288
289 for (std::set<const BookmarkNode*>::iterator it = bookmarks.begin();
290 it != bookmarks.end(); ++it) {
291 const std::set<BookmarkTag>& subset(bookmark_to_tags_[*it]);
292 for (std::set<BookmarkTag>::const_iterator tag_it = subset.begin();
293 tag_it != subset.end(); ++tag_it) {
294 tags[*tag_it] += 1;
295 }
296 }
297 tags.erase(tag); // A tag is not related to itself.
298 }
299
300 std::vector<BookmarkTag> sorted_tags;
301 for (std::map<BookmarkTag, unsigned int>::iterator it = tags.begin();
302 it != tags.end(); ++it) {
303 sorted_tags.push_back(it->first);
304 }
305 std::sort(sorted_tags.begin(), sorted_tags.end(), TagComparator(tags));
306 return sorted_tags;
307 }
308
309 // BookmarkModelObserver methods.
310
311 // Invoked when the model has finished loading.
312 void BookmarkTagModel::Loaded(BookmarkModel* model, bool ids_reassigned) {
313 Load();
314 };
sky 2013/10/16 13:46:19 nit: no ;
noyau (Ping after 24h) 2013/10/16 16:51:32 Done.
315
316 // Invoked from the destructor of the BookmarkModel.
sky 2013/10/16 13:46:19 Are these comments really helpful? We generally do
noyau (Ping after 24h) 2013/10/16 16:51:32 You are right, those were duplicated from the Book
317 void BookmarkTagModel::BookmarkModelBeingDeleted(BookmarkModel* model) {
318 DCHECK(bookmark_model_);
319 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
320 BookmarkTagModelBeingDeleted(this));
321 bookmark_model_ = NULL;
322 }
323
324 // Invoked when a node has moved.
325 void BookmarkTagModel::BookmarkNodeMoved(BookmarkModel* model,
326 const BookmarkNode* old_parent,
327 int old_index,
328 const BookmarkNode* new_parent,
329 int new_index) {
330 DCHECK(loaded_);
331 const BookmarkNode* bookmark = new_parent->GetChild(new_index);
332 std::string encoded;
333 if (!bookmark->GetMetaInfo(TAG_KEY, &encoded)) {
334 // The bookmark moved and the system currently use its ancestors name as a
335 // poor approximation for tags.
336 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
337 OnWillChangeBookmarkTags(this, bookmark));
338 RemoveBookmark(bookmark);
339 LoadBookmark(bookmark);
340 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
341 BookmarkTagsChanged(this, bookmark));
342 }
343 }
344
345 // Invoked when a node has been added.
346 void BookmarkTagModel::BookmarkNodeAdded(BookmarkModel* model,
347 const BookmarkNode* parent,
348 int index) {
349 DCHECK(loaded_);
350 const BookmarkNode* bookmark = parent->GetChild(index);
351 if (!bookmark->is_url())
352 return;
353 LoadBookmark(bookmark);
354
355 if (!inhibit_change_notifications_)
356 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
357 BookmarkNodeAdded(this, bookmark));
358 }
359
360 // Invoked before a node is removed.
361 // |parent| the parent of the node that will be removed.
362 // |old_index| the index of the node about to be removed in |parent|.
363 // |node| is the node to be removed.
364 void BookmarkTagModel::OnWillRemoveBookmarks(BookmarkModel* model,
365 const BookmarkNode* parent,
366 int old_index,
367 const BookmarkNode* node) {
368 DCHECK(loaded_);
369 RemoveBookmark(node);
370 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
371 OnWillRemoveBookmarks(this, node));
372 }
373
374 // Invoked when a node has been removed, the item may still be starred though.
375 // |parent| the parent of the node that was removed.
376 // |old_index| the index of the removed node in |parent| before it was
377 // removed.
378 // |node| is the node that was removed.
379 void BookmarkTagModel::BookmarkNodeRemoved(BookmarkModel* model,
380 const BookmarkNode* parent,
sky 2013/10/16 13:46:19 nit: indentation off.
noyau (Ping after 24h) 2013/10/16 16:51:32 Fixed all of them.
381 int old_index,
382 const BookmarkNode* node) {
383 DCHECK(loaded_);
384 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
385 BookmarkNodeRemoved(this, node));
386 }
387
388 // Invoked before the title or url of a node is changed.
389 void BookmarkTagModel::OnWillChangeBookmarkNode(BookmarkModel* model,
390 const BookmarkNode* node) {
391 DCHECK(loaded_);
392 if (!inhibit_change_notifications_)
393 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
394 OnWillChangeBookmarkNode(this, node));
395 }
396
397 // Invoked when the title or url of a node changes.
398 void BookmarkTagModel::BookmarkNodeChanged(BookmarkModel* model,
399 const BookmarkNode* node) {
400 DCHECK(loaded_);
401 if (node->is_folder()) {
402 // A folder title changed. This may change the tags on all the descendants
sky 2013/10/16 13:46:19 Do you need to remove node too as iteration doesn'
noyau (Ping after 24h) 2013/10/16 16:51:32 There is no mapping kept for folder nodes, RemoveB
403 // still using the default tag list of all ancestors.
404 ExtensiveChanges scoped(ExtensiveChanges(this));
405 ui::TreeNodeIterator<const BookmarkNode> iterator(node);
406 while (iterator.has_next()) {
407 const BookmarkNode* bookmark = iterator.Next();
408 RemoveBookmark(bookmark);
409 LoadBookmark(bookmark);
410 }
411 } else {
412 if (!inhibit_change_notifications_)
413 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
414 BookmarkNodeChanged(this, node));
415 }
416 }
417
418 // Invoked before the metainfo of a node is changed.
419 void BookmarkTagModel::OnWillChangeBookmarkMetaInfo(BookmarkModel* model,
420 const BookmarkNode* node) {
421 DCHECK(loaded_);
422 if (!inhibit_change_notifications_)
423 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
424 OnWillChangeBookmarkTags(this, node));
425 }
426
427 // Invoked when the metainfo on a node changes.
428 void BookmarkTagModel::BookmarkMetaInfoChanged(BookmarkModel* model,
429 const BookmarkNode* node) {
430 DCHECK(loaded_);
431 RemoveBookmark(node);
432 LoadBookmark(node);
433 if (!inhibit_change_notifications_)
434 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
435 BookmarkTagsChanged(this, node));
436 }
437
438 // Invoked when a favicon has been loaded or changed.
439 void BookmarkTagModel::BookmarkNodeFaviconChanged(BookmarkModel* model,
440 const BookmarkNode* node) {
441 DCHECK(loaded_);
442 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
443 BookmarkNodeFaviconChanged(this, node));
444 }
445
446 // Invoked before the direct children of |node| have been reordered in some
447 // way, such as sorted.
448 void BookmarkTagModel::OnWillReorderBookmarkNode(BookmarkModel* model,
449 const BookmarkNode* node) {
450 // This model doesn't care.
451 }
452
453 // Invoked when the children (just direct children, not descendants) of
454 // |node| have been reordered in some way, such as sorted.
455 void BookmarkTagModel::BookmarkNodeChildrenReordered(BookmarkModel* model,
456 const BookmarkNode* node) {
457 // This model doesn't care.
458 }
459
460 // Invoked before an extensive set of model changes is about to begin.
461 // This tells UI intensive observers to wait until the updates finish to
462 // update themselves.
463 // These methods should only be used for imports and sync.
464 // Observers should still respond to BookmarkNodeRemoved immediately,
465 // to avoid holding onto stale node pointers.
466 void BookmarkTagModel::ExtensiveBookmarkChangesBeginning(BookmarkModel* model) {
467 DCHECK(loaded_);
468 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
469 ExtensiveBookmarkChangesBeginning(this));
470 }
471
472 // Invoked after an extensive set of model changes has ended.
473 // This tells observers to update themselves if they were waiting for the
474 // update to finish.
475 void BookmarkTagModel::ExtensiveBookmarkChangesEnded(BookmarkModel* model) {
476 DCHECK(loaded_);
477 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
478 ExtensiveBookmarkChangesEnded(this));
479 }
480
481 // Invoked before all non-permanent bookmark nodes are removed.
482 void BookmarkTagModel::OnWillRemoveAllBookmarks(BookmarkModel* model) {
483 DCHECK(loaded_);
484 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
485 OnWillRemoveAllBookmarks(this));
486 }
487
488 // Invoked when all non-permanent bookmark nodes have been removed.
489 void BookmarkTagModel::BookmarkAllNodesRemoved(BookmarkModel* model){
490 DCHECK(loaded_);
491 tag_to_bookmarks_.clear();
492 bookmark_to_tags_.clear();
493 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
494 BookmarkAllNodesRemoved(this));
495 }
496
497 // Private methods.
498
499 void BookmarkTagModel::SetTagsOnBookmark(
500 const std::set<BookmarkTag>& tags, const BookmarkNode* bookmark) {
501 DCHECK(bookmark_model_);
502 DCHECK(loaded_);
503
504 // Build a ListValue.
505 std::vector<BookmarkTag> tag_vector(tags.begin(), tags.end());
506 base::ListValue list;
507 list.AppendStrings(tag_vector);
508
509 // Encodes it.
510 std::string encoded;
511 JSONStringValueSerializer serializer(&encoded);
512
513 // Pushes it in the bookmark's metainfo. Even if the tag list is empty the
514 // empty list must be put on the node to avoid reverting to the tag list
515 // derived from the hierarchy.
516 // The internal caches of the BookmarkTagModel are updated when the
517 // notification from the BookmarkModel is received.
518 serializer.Serialize(list);
519 bookmark_model_->SetNodeMetaInfo(bookmark, TAG_KEY, encoded);
520 }
521
522 void BookmarkTagModel::Load() {
523 DCHECK(bookmark_model_);
524 DCHECK(!loaded_);
525 ui::TreeNodeIterator<const BookmarkNode> iterator(
526 bookmark_model_->root_node());
527 while (iterator.has_next())
528 LoadBookmark(iterator.Next());
529 loaded_ = true;
530 FOR_EACH_OBSERVER(BookmarkTagModelObserver, observers_,
531 Loaded(this));
532 }
533
534 void BookmarkTagModel::LoadBookmark(const BookmarkNode* bookmark) {
535 DCHECK(bookmark_model_);
536 if (bookmark->is_url()) {
537 std::set<BookmarkTag> tags(ExtractTagsFromBookmark(bookmark));
538
539 bookmark_to_tags_[bookmark] = tags;
540 for (std::set<BookmarkTag>::iterator it = tags.begin();
541 it != tags.end(); ++it) {
542 tag_to_bookmarks_[*it].insert(bookmark);
543 }
544 }
545 }
546
547 void BookmarkTagModel::RemoveBookmark(const BookmarkNode* bookmark) {
548 DCHECK(bookmark_model_);
549 if (bookmark->is_url()) {
550 std::set<BookmarkTag> tags(bookmark_to_tags_[bookmark]);
551 bookmark_to_tags_.erase(bookmark);
552
553 for (std::set<BookmarkTag>::iterator it = tags.begin();
554 it != tags.end(); ++it) {
555 tag_to_bookmarks_[*it].erase(bookmark);
556 // Remove the tags no longer used.
557 if (!tag_to_bookmarks_[*it].size())
558 tag_to_bookmarks_.erase(*it);
559 }
560 }
561 }
OLDNEW
« no previous file with comments | « chrome/browser/bookmarks/bookmark_tag_model.h ('k') | chrome/browser/bookmarks/bookmark_tag_model_observer.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698