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

Side by Side Diff: ui/base/x/x11_window_cache.cc

Issue 2177823002: X11: Add window cache Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Fix API Created 4 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
« no previous file with comments | « ui/base/x/x11_window_cache.h ('k') | ui/base/x/x11_window_cache_unittest.cc » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2016 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 "ui/base/x/x11_window_cache.h"
6
7 #include <X11/Xlib.h>
8 #include <X11/Xlib-xcb.h>
9 #include <xcb/xcbext.h>
10
11 #include <algorithm>
12 #include <cstdio>
13 #include <cstdlib>
14 #include <cstring>
15
16 #include "base/logging.h"
17 #include "ui/base/x/x11_window_event_manager.h"
18 #include "ui/events/platform/platform_event_source.h"
19 #include "ui/events/platform/x11/x11_event_source.h"
20
21 namespace ui {
22
23 namespace {
24
25 using ChildIterator = XWindowCache::Window::Children::iterator;
26
27 const char kNetWmIcon[] = "_NET_WM_ICON";
28
29 // In case a property's value is huge, only cache the first 64KiB, and
30 // indicate in Property that the cached value is not the entire value.
31 static constexpr auto kMaxPropertySize = 0xffff;
32
33 } // anonymous namespace
34
35 // static
36 template <typename T>
37 void XWindowCache::CacheWindowGeometryFromResponse(XWindowCache::Window* window,
38 const T& response) {
39 window->x_ = response.x;
40 window->y_ = response.y;
41 window->width_ = response.width;
42 window->height_ = response.height;
43 window->border_width_ = response.border_width;
44 }
45
46 // static
47 auto XWindowCache::FindChild(XWindowCache::Window* parent,
48 xcb_window_t child_id)
49 -> decltype(parent->children_.begin()) {
50 return std::find_if(parent->children_.begin(), parent->children_.end(),
51 [child_id](std::unique_ptr<XWindowCache::Window>& child) {
52 return child->id() == child_id;
53 });
54 }
55
56 struct XWindowCache::GetWindowAttributesRequest
57 : public X11EventSource::Request {
58 GetWindowAttributesRequest(Window* window, XWindowCache* cache)
59 : Request(xcb_get_window_attributes(cache->connection_, window->id())
60 .sequence),
61 cache_(cache),
62 window_(window) {
63 window_->attributes_request_ = this;
64 }
65
66 void OnReply(xcb_generic_reply_t* r, xcb_generic_error_t* error) override {
67 if (error) {
68 switch (error->error_code) {
69 case BadDrawable:
70 case BadWindow:
71 break;
72 default:
73 NOTREACHED();
74 }
75 cache_->DestroyWindow(window_);
76 return;
77 }
78 auto* reply = reinterpret_cast<xcb_get_window_attributes_reply_t*>(r);
79
80 window_->attributes_request_ = nullptr;
81 window_->override_redirect_ = reply->override_redirect;
82 window_->is_mapped_ = reply->map_state != XCB_MAP_STATE_UNMAPPED;
83 }
84
85 protected:
86 XWindowCache* cache_;
87 Window* window_;
88 };
89
90 struct XWindowCache::GetGeometryRequest : public X11EventSource::Request {
91 GetGeometryRequest(Window* window, XWindowCache* cache)
92 : Request(xcb_get_geometry(cache->connection_, window->id()).sequence),
93 cache_(cache),
94 window_(window) {
95 window_->geometry_request_ = this;
96 }
97
98 void OnReply(xcb_generic_reply_t* r, xcb_generic_error_t* error) override {
99 if (error) {
100 switch (error->error_code) {
101 case BadDrawable:
102 break;
103 default:
104 NOTREACHED();
105 }
106 cache_->DestroyWindow(window_);
107 return;
108 }
109 auto* reply = reinterpret_cast<xcb_get_geometry_reply_t*>(r);
110
111 window_->geometry_request_ = nullptr;
112 CacheWindowGeometryFromResponse(window_, *reply);
113 }
114
115 protected:
116 XWindowCache* cache_;
117 Window* window_;
118 };
119
120 struct XWindowCache::ListPropertiesRequest : public X11EventSource::Request {
121 ListPropertiesRequest(Window* window, XWindowCache* cache)
122 : Request(xcb_list_properties(cache->connection_, window->id()).sequence),
123 cache_(cache),
124 window_(window) {
125 window_->properties_request_ = this;
126 }
127
128 void OnReply(xcb_generic_reply_t* r, xcb_generic_error_t* error) override {
129 if (error) {
130 switch (error->error_code) {
131 case BadWindow:
132 break;
133 default:
134 NOTREACHED();
135 }
136 cache_->DestroyWindow(window_);
137 return;
138 }
139 auto* reply = reinterpret_cast<xcb_list_properties_reply_t*>(r);
140
141 window_->properties_request_ = nullptr;
142 for (int i = 0; i < xcb_list_properties_atoms_length(reply); i++) {
143 xcb_atom_t atom = xcb_list_properties_atoms(reply)[i];
144 cache_->CacheProperty(window_, atom);
145 }
146 }
147
148 protected:
149 XWindowCache* cache_;
150 Window* window_;
151 };
152
153 struct XWindowCache::QueryTreeRequest : public X11EventSource::Request {
154 QueryTreeRequest(Window* window, XWindowCache* cache)
155 : Request(xcb_query_tree(cache->connection_, window->id()).sequence),
156 cache_(cache),
157 window_(window) {
158 window_->children_request_ = this;
159 }
160
161 void OnReply(xcb_generic_reply_t* r, xcb_generic_error_t* error) override {
162 if (error) {
163 switch (error->error_code) {
164 case BadWindow:
165 break;
166 default:
167 NOTREACHED();
168 }
169 cache_->DestroyWindow(window_);
170 return;
171 }
172 auto* reply = reinterpret_cast<xcb_query_tree_reply_t*>(r);
173
174 DCHECK(window_->children_.empty());
175
176 xcb_window_t* children = xcb_query_tree_children(reply);
177 int n_children = xcb_query_tree_children_length(reply);
178
179 window_->children_request_ = nullptr;
180
181 // Iterate over children from top-to-bottom.
182 for (int i = 0; i < n_children; i++)
183 cache_->CreateWindow(children[i], window_);
184 }
185
186 protected:
187 XWindowCache* cache_;
188 Window* window_;
189 };
190
191 struct XWindowCache::GetPropertyRequest : public X11EventSource::Request {
192 GetPropertyRequest(Window* window,
193 Property* property,
194 xcb_atom_t atom,
195 XWindowCache* cache)
196 : Request(xcb_get_property(cache->connection_,
197 false,
198 window->id(),
199 atom,
200 XCB_ATOM_ANY,
201 0,
202 kMaxPropertySize)
203 .sequence),
204 cache_(cache),
205 window_(window),
206 property_(property),
207 atom_(atom) {
208 property->property_request_ = this;
209 }
210
211 void OnReply(xcb_generic_reply_t* r, xcb_generic_error_t* error) override {
212 if (error) {
213 switch (error->error_code) {
214 case BadWindow:
215 break;
216 case BadValue:
217 case BadAtom:
218 default:
219 NOTREACHED();
220 }
221 // Destruct the |window_| later because clients may still want
222 // its info, but remove the property.
223 window_->properties_.erase(atom_);
224 return;
225 }
226 auto* reply = reinterpret_cast<xcb_get_property_reply_t*>(r);
227
228 if (reply->format == 0) {
229 // According to Xlib, a format of anything other than 8, 16, or
230 // 32 is a BadImplementation error. However, this occurs when
231 // creating a new xterm window, so just forget about the
232 // property in question.
233 window_->properties_.erase(atom_);
234 return;
235 }
236
237 property_->property_request_ = nullptr;
238 property_->type_ = reply->type;
239 DCHECK(reply->format == 8 || reply->format == 16 || reply->format == 32);
240 property_->format_ = reply->format;
241 property_->length_ = xcb_get_property_value_length(reply);
242 uint32_t data_bytes = property_->length_ * (property_->format_ / 8);
243 property_->data_ = new uint8_t[data_bytes];
244 std::memcpy(property_->data_, xcb_get_property_value(reply), data_bytes);
245 }
246
247 protected:
248 XWindowCache* cache_;
249 Window* window_;
250 Property* property_;
251 xcb_atom_t atom_;
252 };
253
254 ////////////////////////////////////////////////////////////////////////////////
255 // XWindowCache
256
257 // Xlib shall own the event queue.
258 XWindowCache::XWindowCache(XDisplay* display,
259 X11EventSource* event_source,
260 XID root)
261 : display_(display),
262 connection_(XGetXCBConnection(display_)),
263 root_id_(root),
264 event_source_(event_source),
265 root_(nullptr),
266 net_wm_icon_(0) {
267 DCHECK(event_source);
268 DCHECK(!xcb_connection_has_error(connection_));
269
270 if (PlatformEventSource::GetInstance())
271 PlatformEventSource::GetInstance()->AddPlatformEventObserver(this);
272
273 net_wm_icon_cookie_ =
274 xcb_intern_atom(connection_, false, sizeof(kNetWmIcon) - 1, kNetWmIcon);
275
276 CreateWindow(root, nullptr);
277 }
278
279 XWindowCache::~XWindowCache() {
280 if (PlatformEventSource::GetInstance())
281 PlatformEventSource::GetInstance()->RemovePlatformEventObserver(this);
282 }
283
284 const XWindowCache::Window* XWindowCache::GetWindow(XID id) const {
285 Window* window = GetWindowInternal(id);
286 if (!window) {
287 // The window may not be cached yet, so fall back on a BFS (don't
288 // use a DFS here because we spend less time blocking if we cache
289 // top-down).
290 std::queue<Window*> q;
291 if (root_)
292 q.push(root_.get());
293 while (!q.empty()) {
294 Window* next = q.front();
295 if(next->id() == id) {
296 window = next;
297 break;
298 }
299 q.pop();
300 if (!next->children_request_ ||
301 event_source_->DispatchRequestNow(next->children_request_)) {
302 for (auto& child : next->children_)
303 q.push(child.get());
304 }
305 }
306 }
307 return window && window->Validate() ? window : nullptr;
308 }
309
310 XWindowCache::Window* XWindowCache::GetWindowInternal(XID id) const {
311 auto it = windows_.find(id);
312 return it == windows_.end() ? nullptr : it->second;
313 }
314
315 // TODO(thomasanderson): Call ProcessEvent directly from X11EventSource. Have
316 // ProcessEvent return an indication of if it is possible for clients other
317 // than XWindowCache to be interested in the event, so that event dispatchers
318 // don't get bogged down with the many events that XWindowCache selects.
319 void XWindowCache::WillProcessEvent(const PlatformEvent& event) {
320 ProcessEvent(event);
321 }
322
323 void XWindowCache::DidProcessEvent(const PlatformEvent& event) {}
324
325 void XWindowCache::ProcessEvent(const XEvent* e) {
326 switch (e->type) {
327 case PropertyNotify: {
328 Window* window = GetWindowInternal(e->xproperty.window);
329 if (!window)
330 break;
331
332 switch (e->xproperty.state) {
333 case PropertyDelete:
334 window->properties_.erase(e->xproperty.atom);
335 break;
336 case PropertyNewValue:
337 CacheProperty(window, e->xproperty.atom);
338 break;
339 default:
340 NOTREACHED();
341 }
342 break;
343 }
344 case CirculateNotify: {
345 Window* window = GetWindowInternal(e->xcirculate.window);
346 if (!window)
347 break;
348
349 if (e->xcirculate.event == e->xcirculate.window)
350 break; // This is our root window
351
352 Window* parent = window->parent_;
353 if (parent->id() != e->xcirculate.event)
354 ResetCache();
355
356 auto it = FindChild(parent, window->id());
357 switch (e->xcirculate.place) {
358 case PlaceOnTop:
359 parent->children_.push_front(std::move(*it));
360 break;
361 case PlaceOnBottom:
362 parent->children_.push_back(std::move(*it));
363 break;
364 default:
365 NOTREACHED();
366 }
367 parent->children_.erase(it);
368 break;
369 }
370 case ConfigureNotify: {
371 Window* window = GetWindowInternal(e->xconfigure.window);
372 if (!window)
373 break;
374
375 CacheWindowGeometryFromResponse(window, e->xconfigure);
376
377 if (e->xconfigure.event == e->xconfigure.window)
378 break;
379
380 Window* parent = window->parent_;
381 if (parent->id() != e->xconfigure.event)
382 ResetCache();
383
384 auto it = FindChild(parent, window->id());
385 if (e->xconfigure.above) {
386 auto it_above = FindChild(parent, e->xconfigure.above);
387 if (it == parent->children_.end())
388 ResetCache();
389 else
390 parent->children_.insert(it_above, std::move(*it));
391 } else {
392 // |window| is not above any other sibling window
393 parent->children_.push_back(std::move(*it));
394 }
395 parent->children_.erase(it);
396 break;
397 }
398 case CreateNotify: {
399 Window* parent = GetWindowInternal(e->xcreatewindow.parent);
400 if (!parent)
401 break;
402
403 if (parent->children_request_) {
404 // |parent| is in the process of being cached, so we will pick up this
405 // window in the near future.
406 break;
407 }
408
409 CreateWindow(e->xcreatewindow.window, parent);
410 break;
411 }
412 case DestroyNotify: {
413 Window* window = GetWindowInternal(e->xdestroywindow.window);
414 if (!window)
415 break;
416 DestroyWindow(window);
417 break;
418 }
419 case GravityNotify: {
420 Window* window = GetWindowInternal(e->xgravity.window);
421 if (!window)
422 break;
423
424 window->x_ = e->xgravity.x;
425 window->y_ = e->xgravity.y;
426 break;
427 }
428 case MapNotify: {
429 Window* window = GetWindowInternal(e->xmap.window);
430 if (!window)
431 break;
432
433 window->override_redirect_ = e->xmap.override_redirect;
434 window->is_mapped_ = true;
435 break;
436 }
437 case ReparentNotify: {
438 Window* window = GetWindowInternal(e->xreparent.window);
439 if (!window)
440 break;
441
442 window->x_ = e->xreparent.x;
443 window->y_ = e->xreparent.y;
444
445 window->override_redirect_ = e->xreparent.override_redirect;
446 window->is_mapped_ = false; // Reparenting a window unmaps it
447
448 Window* old_parent = window->parent_;
449 if (!old_parent)
450 break; // Don't worry about caching windows above our root.
451
452 Window* new_parent = GetWindowInternal(e->xreparent.parent);
453 if (!new_parent || new_parent->children_request_) {
454 // |window| is either no longer in our tree, or we are already
455 // waiting to receive a list of |new_parent|'s children. We
456 // conservatively throw away |window| in the second case
457 // because of a race condition where (eg.) |window| gets
458 // reparented to |new_parent| and changes its size before the
459 // server has received our request to select substructure
460 // events on |new_parent|. We could avoid this by selecting
461 // structure events on |window|, but we need to select
462 // substructure events on all windows anyway to receive
463 // CreateNotify events, and we don't want to 2 duplicate
464 // events every time a window moves or is resized.
465 DestroyWindow(window);
466 break;
467 }
468 window->parent_ = new_parent;
469
470 auto it = FindChild(old_parent, window->id());
471 new_parent->children_.push_front(std::move(*it));
472 old_parent->children_.erase(it);
473 break;
474 }
475 case UnmapNotify: {
476 Window* window = GetWindowInternal(e->xunmap.window);
477 if (!window)
478 break;
479
480 window->is_mapped_ = false;
481 break;
482 }
483 default:
484 break;
485 }
486 }
487
488 void XWindowCache::ResetCache() {
489 NOTREACHED();
490
491 // On release builds, try to fix our state.
492 ResetCacheImpl();
493 }
494
495 void XWindowCache::ResetCacheImpl() {
496 // TODO(thomasanderson): Log something in UMA.
497 if (root_) {
498 DestroyWindow(root_.get());
499 DCHECK(windows_.empty());
500 CreateWindow(root_id_, nullptr);
501 }
502 }
503
504 void XWindowCache::CacheProperty(XWindowCache::Window* window,
505 xcb_atom_t atom) {
506 if (!net_wm_icon_ && net_wm_icon_cookie_.sequence) {
507 auto reply =
508 xcb_intern_atom_reply(connection_, net_wm_icon_cookie_, nullptr);
509 if (reply) {
510 net_wm_icon_ = reply->atom;
511 free(reply);
512 }
513 net_wm_icon_cookie_.sequence = 0;
514 }
515 if (atom == net_wm_icon_)
516 return;
517 window->properties_[atom].reset(new Property(atom, window, this));
518 }
519
520 void XWindowCache::CreateWindow(xcb_window_t id, XWindowCache::Window* parent) {
521 auto it = windows_.find(id);
522 if (it != windows_.end()) {
523 // We're already tracking window |id|.
524 return;
525 }
526
527 Window* window = new Window(id, parent, this);
528 windows_[id] = window;
529 if (parent) {
530 parent->children_.emplace_front(window);
531 } else {
532 DCHECK(!root_);
533 root_.reset(window);
534 }
535 }
536
537 void XWindowCache::DestroyWindow(Window* window) {
538 DCHECK(window);
539 xcb_window_t id = window->id();
540 if (window->parent_) {
541 auto it = FindChild(window->parent_, window->id());
542 DCHECK(it != window->parent_->children_.end());
543 window->parent_->children_.erase(it);
544 } else {
545 DCHECK_EQ(window, root_.get());
546 root_.reset();
547 }
548 windows_.erase(id);
549 }
550
551 ////////////////////////////////////////////////////////////////////////////////
552 // XWindowCache::Property
553
554 XWindowCache::Property::Property(xcb_atom_t name,
555 Window* window,
556 XWindowCache* cache)
557 : name_(name), property_request_(nullptr), data_(nullptr), cache_(cache) {
558 cache->event_source_->EnqueueRequest(
559 new GetPropertyRequest(window, this, name, cache));
560 xcb_flush(cache->connection_);
561 }
562
563 XWindowCache::Property::~Property() {
564 if (property_request_)
565 cache_->event_source_->DiscardRequest(property_request_);
566
567 delete[] data_;
568 }
569
570 bool XWindowCache::Property::Validate() {
571 return (!property_request_ ||
572 cache_->event_source_->DispatchRequestNow(property_request_));
573 }
574
575 ////////////////////////////////////////////////////////////////////////////////
576 // XWindowCache::Window
577
578 XWindowCache::Window::Window(xcb_window_t id,
579 XWindowCache::Window* parent,
580 XWindowCache* cache)
581 : id_(id),
582 parent_(parent),
583 attributes_request_(nullptr),
584 geometry_request_(nullptr),
585 properties_request_(nullptr),
586 children_request_(nullptr),
587 cache_(cache) {
588 // Select state change events BEFORE getting the initial window state to avoid
589 // race conditions.
590 uint32_t event_mask =
591 XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_PROPERTY_CHANGE;
592 if (id == cache->root_id_)
593 event_mask |= XCB_EVENT_MASK_STRUCTURE_NOTIFY;
594 selected_events_.reset(new XScopedEventSelector(id, event_mask));
595
596 cache->event_source_->EnqueueRequest(new QueryTreeRequest(this, cache));
597 cache->event_source_->EnqueueRequest(new ListPropertiesRequest(this, cache));
598 cache->event_source_->EnqueueRequest(new GetGeometryRequest(this, cache));
599 cache->event_source_->EnqueueRequest(
600 new GetWindowAttributesRequest(this, cache));
601 xcb_flush(cache->connection_);
602 }
603
604 XWindowCache::Window::~Window() {
605 // The window tree was created top-down, so must be destroyed bottom-up.
606 while (!children_.empty())
607 cache_->DestroyWindow(children_.front().get());
608
609 if (attributes_request_)
610 cache_->event_source_->DiscardRequest(attributes_request_);
611 if (geometry_request_)
612 cache_->event_source_->DiscardRequest(geometry_request_);
613 if (properties_request_)
614 cache_->event_source_->DiscardRequest(properties_request_);
615 if (children_request_)
616 cache_->event_source_->DiscardRequest(children_request_);
617 }
618
619 bool XWindowCache::Window::Validate() {
620 auto es = cache_->event_source_;
621 if ((children_request_ && !es->DispatchRequestNow(children_request_)) ||
622 (properties_request_ && !es->DispatchRequestNow(properties_request_)) ||
623 (geometry_request_ && !es->DispatchRequestNow(geometry_request_)) ||
624 (attributes_request_ && !es->DispatchRequestNow(attributes_request_)))
625 return false;
626
627 return true;
628 }
629
630 bool XWindowCache::Window::IsValid() const {
631 return !children_request_ && !properties_request_ && !geometry_request_ &&
632 !attributes_request_;
633 }
634
635 const XWindowCache::Property* XWindowCache::Window::GetProperty(
636 XID atom) const {
637 // |window_| should already be validated for clients to be able to
638 // call this function
639 DCHECK(IsValid());
640 auto it = properties_.find(atom);
641 if (it == properties_.end())
642 return nullptr;
643 Property* property = it->second.get();
644 return property->Validate() ? property : nullptr;
645 }
646
647 XWindowCache::Window::Children XWindowCache::Window::GetChildren() const {
648 return Children(this);
649 }
650
651 ////////////////////////////////////////////////////////////////////////////////
652 // XWindowCache::Window::Children
653
654 XWindowCache::Window::Children::Children(const XWindowCache::Window* window)
655 : window_(window) {}
656
657 std::size_t XWindowCache::Window::Children::size() const {
658 std::size_t count = 0;
659 for (const auto* window : *this) {
660 (void)window;
661 count++;
662 }
663 return count;
664 }
665
666 ChildIterator XWindowCache::Window::Children::begin() const {
667 return iterator(window_->children_.begin(), window_->children_.end());
668 }
669
670 ChildIterator XWindowCache::Window::Children::end() const {
671 return iterator(window_->children_.end(), window_->children_.end());
672 }
673
674 ////////////////////////////////////////////////////////////////////////////////
675 // XWindowCache::Window::Children::iterator
676
677 ChildIterator::iterator(std::list<std::unique_ptr<Window>>::const_iterator it,
678 std::list<std::unique_ptr<Window>>::const_iterator end)
679 : it_(it), end_(end) {
680 SkipInvalidChildren();
681 }
682
683 ChildIterator::iterator(const iterator& other)
684 : it_(other.it_), end_(other.end_) {}
685
686 ChildIterator::~iterator() {}
687
688 auto ChildIterator::operator++() -> iterator& {
689 it_++;
690 SkipInvalidChildren();
691 return *this;
692 }
693
694 bool ChildIterator::operator==(const iterator& other) const {
695 return it_ == other.it_;
696 }
697
698 bool ChildIterator::operator!=(const iterator& other) const {
699 return it_ != other.it_;
700 }
701
702 const XWindowCache::Window* ChildIterator::operator*() const {
703 return it_->get();
704 }
705
706 // The CAP theorem says we can only guarantee two out of (Consistency,
707 // Availability, Partition Tolerance). We want to guarantee
708 // availability (ie, when a client requests window information, we
709 // want to at least give them something) and partition tolerance (we
710 // want to return an atomic window, not one that was destroyed
711 // half-way through caching that we can no longer obtain information
712 // about). XWindowCache sacrifices consistency and settles for BASE
713 // (Basically Available, Soft state, Eventual consistency) semantics.
714 // There are cases when the cache may not be entirely built, but
715 // clients need window information. In this case, we must process
716 // events and replies out-of-order to obtain the necessary
717 // information. By doing this, we sacrifice consistency for
718 // availability. However, we select events on windows before caching
719 // their information, so even though we early-process replies, the
720 // queued events will bring the window to eventual consistency.
721 void ChildIterator::SkipInvalidChildren() {
722 while (it_ != end_) {
723 auto next = it_;
724 next++;
725 if ((*it_)->Validate())
726 return;
727 it_ = next;
728 }
729 }
730
731 } // namespace ui
OLDNEW
« no previous file with comments | « ui/base/x/x11_window_cache.h ('k') | ui/base/x/x11_window_cache_unittest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698