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

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

Issue 860873002: Continue deleting code in ui/. (Closed) Base URL: https://github.com/domokit/mojo.git@master
Patch Set: 2015 Created 5 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
« no previous file with comments | « ui/base/x/x11_util.h ('k') | ui/base/x/x11_util_internal.h » ('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 (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.
4
5 // This file defines utility functions for X11 (Linux only). This code has been
6 // ported from XCB since we can't use XCB on Ubuntu while its 32-bit support
7 // remains woefully incomplete.
8
9 #include "ui/base/x/x11_util.h"
10
11 #include <ctype.h>
12 #include <sys/ipc.h>
13 #include <sys/shm.h>
14
15 #include <list>
16 #include <map>
17 #include <utility>
18 #include <vector>
19
20 #include <X11/extensions/shape.h>
21 #include <X11/extensions/XInput2.h>
22 #include <X11/Xcursor/Xcursor.h>
23
24 #include "base/bind.h"
25 #include "base/debug/trace_event.h"
26 #include "base/logging.h"
27 #include "base/memory/scoped_ptr.h"
28 #include "base/memory/singleton.h"
29 #include "base/message_loop/message_loop.h"
30 #include "base/metrics/histogram.h"
31 #include "base/strings/string_number_conversions.h"
32 #include "base/strings/string_util.h"
33 #include "base/strings/stringprintf.h"
34 #include "base/sys_byteorder.h"
35 #include "base/threading/thread.h"
36 #include "skia/ext/image_operations.h"
37 #include "third_party/skia/include/core/SkBitmap.h"
38 #include "third_party/skia/include/core/SkPostConfig.h"
39 #include "ui/base/x/x11_util_internal.h"
40 #include "ui/events/event_utils.h"
41 #include "ui/events/keycodes/keyboard_code_conversion_x.h"
42 #include "ui/events/x/device_data_manager_x11.h"
43 #include "ui/events/x/touch_factory_x11.h"
44 #include "ui/gfx/geometry/insets.h"
45 #include "ui/gfx/geometry/point.h"
46 #include "ui/gfx/geometry/point_conversions.h"
47 #include "ui/gfx/geometry/rect.h"
48 #include "ui/gfx/geometry/size.h"
49 #include "ui/gfx/skia_util.h"
50 #include "ui/gfx/x/x11_error_tracker.h"
51
52 #if defined(OS_FREEBSD)
53 #include <sys/sysctl.h>
54 #include <sys/types.h>
55 #endif
56
57 namespace ui {
58
59 namespace {
60
61 int DefaultX11ErrorHandler(XDisplay* d, XErrorEvent* e) {
62 if (base::MessageLoop::current()) {
63 base::MessageLoop::current()->PostTask(
64 FROM_HERE, base::Bind(&LogErrorEventDescription, d, *e));
65 } else {
66 LOG(ERROR)
67 << "X error received: "
68 << "serial " << e->serial << ", "
69 << "error_code " << static_cast<int>(e->error_code) << ", "
70 << "request_code " << static_cast<int>(e->request_code) << ", "
71 << "minor_code " << static_cast<int>(e->minor_code);
72 }
73 return 0;
74 }
75
76 int DefaultX11IOErrorHandler(XDisplay* d) {
77 // If there's an IO error it likely means the X server has gone away
78 LOG(ERROR) << "X IO error received (X server probably went away)";
79 _exit(1);
80 }
81
82 // Note: The caller should free the resulting value data.
83 bool GetProperty(XID window, const std::string& property_name, long max_length,
84 XAtom* type, int* format, unsigned long* num_items,
85 unsigned char** property) {
86 XAtom property_atom = GetAtom(property_name.c_str());
87 unsigned long remaining_bytes = 0;
88 return XGetWindowProperty(gfx::GetXDisplay(),
89 window,
90 property_atom,
91 0, // offset into property data to read
92 max_length, // max length to get
93 False, // deleted
94 AnyPropertyType,
95 type,
96 format,
97 num_items,
98 &remaining_bytes,
99 property);
100 }
101
102 bool SupportsEWMH() {
103 static bool supports_ewmh = false;
104 static bool supports_ewmh_cached = false;
105 if (!supports_ewmh_cached) {
106 supports_ewmh_cached = true;
107
108 int wm_window = 0u;
109 if (!GetIntProperty(GetX11RootWindow(),
110 "_NET_SUPPORTING_WM_CHECK",
111 &wm_window)) {
112 supports_ewmh = false;
113 return false;
114 }
115
116 // It's possible that a window manager started earlier in this X session
117 // left a stale _NET_SUPPORTING_WM_CHECK property when it was replaced by a
118 // non-EWMH window manager, so we trap errors in the following requests to
119 // avoid crashes (issue 23860).
120
121 // EWMH requires the supporting-WM window to also have a
122 // _NET_SUPPORTING_WM_CHECK property pointing to itself (to avoid a stale
123 // property referencing an ID that's been recycled for another window), so
124 // we check that too.
125 gfx::X11ErrorTracker err_tracker;
126 int wm_window_property = 0;
127 bool result = GetIntProperty(
128 wm_window, "_NET_SUPPORTING_WM_CHECK", &wm_window_property);
129 supports_ewmh = !err_tracker.FoundNewError() &&
130 result &&
131 wm_window_property == wm_window;
132 }
133
134 return supports_ewmh;
135 }
136
137 bool GetWindowManagerName(std::string* wm_name) {
138 DCHECK(wm_name);
139 if (!SupportsEWMH())
140 return false;
141
142 int wm_window = 0;
143 if (!GetIntProperty(GetX11RootWindow(),
144 "_NET_SUPPORTING_WM_CHECK",
145 &wm_window)) {
146 return false;
147 }
148
149 gfx::X11ErrorTracker err_tracker;
150 bool result = GetStringProperty(
151 static_cast<XID>(wm_window), "_NET_WM_NAME", wm_name);
152 return !err_tracker.FoundNewError() && result;
153 }
154
155 // A process wide singleton that manages the usage of X cursors.
156 class XCursorCache {
157 public:
158 XCursorCache() {}
159 ~XCursorCache() {
160 Clear();
161 }
162
163 ::Cursor GetCursor(int cursor_shape) {
164 // Lookup cursor by attempting to insert a null value, which avoids
165 // a second pass through the map after a cache miss.
166 std::pair<std::map<int, ::Cursor>::iterator, bool> it = cache_.insert(
167 std::make_pair(cursor_shape, 0));
168 if (it.second) {
169 XDisplay* display = gfx::GetXDisplay();
170 it.first->second = XCreateFontCursor(display, cursor_shape);
171 }
172 return it.first->second;
173 }
174
175 void Clear() {
176 XDisplay* display = gfx::GetXDisplay();
177 for (std::map<int, ::Cursor>::iterator it =
178 cache_.begin(); it != cache_.end(); ++it) {
179 XFreeCursor(display, it->second);
180 }
181 cache_.clear();
182 }
183
184 private:
185 // Maps X11 font cursor shapes to Cursor IDs.
186 std::map<int, ::Cursor> cache_;
187
188 DISALLOW_COPY_AND_ASSIGN(XCursorCache);
189 };
190
191 XCursorCache* cursor_cache = NULL;
192
193 // A process wide singleton cache for custom X cursors.
194 class XCustomCursorCache {
195 public:
196 static XCustomCursorCache* GetInstance() {
197 return Singleton<XCustomCursorCache>::get();
198 }
199
200 ::Cursor InstallCustomCursor(XcursorImage* image) {
201 XCustomCursor* custom_cursor = new XCustomCursor(image);
202 ::Cursor xcursor = custom_cursor->cursor();
203 cache_[xcursor] = custom_cursor;
204 return xcursor;
205 }
206
207 void Ref(::Cursor cursor) {
208 cache_[cursor]->Ref();
209 }
210
211 void Unref(::Cursor cursor) {
212 if (cache_[cursor]->Unref())
213 cache_.erase(cursor);
214 }
215
216 void Clear() {
217 cache_.clear();
218 }
219
220 const XcursorImage* GetXcursorImage(::Cursor cursor) const {
221 return cache_.find(cursor)->second->image();
222 }
223
224 private:
225 friend struct DefaultSingletonTraits<XCustomCursorCache>;
226
227 class XCustomCursor {
228 public:
229 // This takes ownership of the image.
230 XCustomCursor(XcursorImage* image)
231 : image_(image),
232 ref_(1) {
233 cursor_ = XcursorImageLoadCursor(gfx::GetXDisplay(), image);
234 }
235
236 ~XCustomCursor() {
237 XcursorImageDestroy(image_);
238 XFreeCursor(gfx::GetXDisplay(), cursor_);
239 }
240
241 ::Cursor cursor() const { return cursor_; }
242
243 void Ref() {
244 ++ref_;
245 }
246
247 // Returns true if the cursor was destroyed because of the unref.
248 bool Unref() {
249 if (--ref_ == 0) {
250 delete this;
251 return true;
252 }
253 return false;
254 }
255
256 const XcursorImage* image() const {
257 return image_;
258 };
259
260 private:
261 XcursorImage* image_;
262 int ref_;
263 ::Cursor cursor_;
264
265 DISALLOW_COPY_AND_ASSIGN(XCustomCursor);
266 };
267
268 XCustomCursorCache() {}
269 ~XCustomCursorCache() {
270 Clear();
271 }
272
273 std::map< ::Cursor, XCustomCursor*> cache_;
274 DISALLOW_COPY_AND_ASSIGN(XCustomCursorCache);
275 };
276
277 } // namespace
278
279 bool IsXInput2Available() {
280 return DeviceDataManagerX11::GetInstance()->IsXInput2Available();
281 }
282
283 static SharedMemorySupport DoQuerySharedMemorySupport(XDisplay* dpy) {
284 int dummy;
285 Bool pixmaps_supported;
286 // Query the server's support for XSHM.
287 if (!XShmQueryVersion(dpy, &dummy, &dummy, &pixmaps_supported))
288 return SHARED_MEMORY_NONE;
289
290 #if defined(OS_FREEBSD)
291 // On FreeBSD we can't access the shared memory after it was marked for
292 // deletion, unless this behaviour is explicitly enabled by the user.
293 // In case it's not enabled disable shared memory support.
294 int allow_removed;
295 size_t length = sizeof(allow_removed);
296
297 if ((sysctlbyname("kern.ipc.shm_allow_removed", &allow_removed, &length,
298 NULL, 0) < 0) || allow_removed < 1) {
299 return SHARED_MEMORY_NONE;
300 }
301 #endif
302
303 // Next we probe to see if shared memory will really work
304 int shmkey = shmget(IPC_PRIVATE, 1, 0600);
305 if (shmkey == -1) {
306 LOG(WARNING) << "Failed to get shared memory segment.";
307 return SHARED_MEMORY_NONE;
308 } else {
309 VLOG(1) << "Got shared memory segment " << shmkey;
310 }
311
312 void* address = shmat(shmkey, NULL, 0);
313 // Mark the shared memory region for deletion
314 shmctl(shmkey, IPC_RMID, NULL);
315
316 XShmSegmentInfo shminfo;
317 memset(&shminfo, 0, sizeof(shminfo));
318 shminfo.shmid = shmkey;
319
320 gfx::X11ErrorTracker err_tracker;
321 bool result = XShmAttach(dpy, &shminfo);
322 if (result)
323 VLOG(1) << "X got shared memory segment " << shmkey;
324 else
325 LOG(WARNING) << "X failed to attach to shared memory segment " << shmkey;
326 if (err_tracker.FoundNewError())
327 result = false;
328 shmdt(address);
329 if (!result) {
330 LOG(WARNING) << "X failed to attach to shared memory segment " << shmkey;
331 return SHARED_MEMORY_NONE;
332 }
333
334 VLOG(1) << "X attached to shared memory segment " << shmkey;
335
336 XShmDetach(dpy, &shminfo);
337 return pixmaps_supported ? SHARED_MEMORY_PIXMAP : SHARED_MEMORY_PUTIMAGE;
338 }
339
340 SharedMemorySupport QuerySharedMemorySupport(XDisplay* dpy) {
341 static SharedMemorySupport shared_memory_support = SHARED_MEMORY_NONE;
342 static bool shared_memory_support_cached = false;
343
344 if (shared_memory_support_cached)
345 return shared_memory_support;
346
347 shared_memory_support = DoQuerySharedMemorySupport(dpy);
348 shared_memory_support_cached = true;
349
350 return shared_memory_support;
351 }
352
353 bool QueryRenderSupport(Display* dpy) {
354 int dummy;
355 // We don't care about the version of Xrender since all the features which
356 // we use are included in every version.
357 static bool render_supported = XRenderQueryExtension(dpy, &dummy, &dummy);
358
359 return render_supported;
360 }
361
362 ::Cursor GetXCursor(int cursor_shape) {
363 if (!cursor_cache)
364 cursor_cache = new XCursorCache;
365 return cursor_cache->GetCursor(cursor_shape);
366 }
367
368 ::Cursor CreateReffedCustomXCursor(XcursorImage* image) {
369 return XCustomCursorCache::GetInstance()->InstallCustomCursor(image);
370 }
371
372 void RefCustomXCursor(::Cursor cursor) {
373 XCustomCursorCache::GetInstance()->Ref(cursor);
374 }
375
376 void UnrefCustomXCursor(::Cursor cursor) {
377 XCustomCursorCache::GetInstance()->Unref(cursor);
378 }
379
380 XcursorImage* SkBitmapToXcursorImage(const SkBitmap* cursor_image,
381 const gfx::Point& hotspot) {
382 DCHECK(cursor_image->colorType() == kN32_SkColorType);
383 gfx::Point hotspot_point = hotspot;
384 SkBitmap scaled;
385
386 // X11 seems to have issues with cursors when images get larger than 64
387 // pixels. So rescale the image if necessary.
388 const float kMaxPixel = 64.f;
389 bool needs_scale = false;
390 if (cursor_image->width() > kMaxPixel || cursor_image->height() > kMaxPixel) {
391 float scale = 1.f;
392 if (cursor_image->width() > cursor_image->height())
393 scale = kMaxPixel / cursor_image->width();
394 else
395 scale = kMaxPixel / cursor_image->height();
396
397 scaled = skia::ImageOperations::Resize(*cursor_image,
398 skia::ImageOperations::RESIZE_BETTER,
399 static_cast<int>(cursor_image->width() * scale),
400 static_cast<int>(cursor_image->height() * scale));
401 hotspot_point = gfx::ToFlooredPoint(gfx::ScalePoint(hotspot, scale));
402 needs_scale = true;
403 }
404
405 const SkBitmap* bitmap = needs_scale ? &scaled : cursor_image;
406 XcursorImage* image = XcursorImageCreate(bitmap->width(), bitmap->height());
407 image->xhot = std::min(bitmap->width() - 1, hotspot_point.x());
408 image->yhot = std::min(bitmap->height() - 1, hotspot_point.y());
409
410 if (bitmap->width() && bitmap->height()) {
411 bitmap->lockPixels();
412 // The |bitmap| contains ARGB image, so just copy it.
413 memcpy(image->pixels,
414 bitmap->getPixels(),
415 bitmap->width() * bitmap->height() * 4);
416 bitmap->unlockPixels();
417 }
418
419 return image;
420 }
421
422
423 int CoalescePendingMotionEvents(const XEvent* xev,
424 XEvent* last_event) {
425 XIDeviceEvent* xievent = static_cast<XIDeviceEvent*>(xev->xcookie.data);
426 int num_coalesced = 0;
427 XDisplay* display = xev->xany.display;
428 int event_type = xev->xgeneric.evtype;
429
430 DCHECK(event_type == XI_Motion || event_type == XI_TouchUpdate);
431
432 while (XPending(display)) {
433 XEvent next_event;
434 XPeekEvent(display, &next_event);
435
436 // If we can't get the cookie, abort the check.
437 if (!XGetEventData(next_event.xgeneric.display, &next_event.xcookie))
438 return num_coalesced;
439
440 // If this isn't from a valid device, throw the event away, as
441 // that's what the message pump would do. Device events come in pairs
442 // with one from the master and one from the slave so there will
443 // always be at least one pending.
444 if (!ui::TouchFactory::GetInstance()->ShouldProcessXI2Event(&next_event)) {
445 XFreeEventData(display, &next_event.xcookie);
446 XNextEvent(display, &next_event);
447 continue;
448 }
449
450 if (next_event.type == GenericEvent &&
451 next_event.xgeneric.evtype == event_type &&
452 !ui::DeviceDataManagerX11::GetInstance()->IsCMTGestureEvent(
453 &next_event)) {
454 XIDeviceEvent* next_xievent =
455 static_cast<XIDeviceEvent*>(next_event.xcookie.data);
456 // Confirm that the motion event is targeted at the same window
457 // and that no buttons or modifiers have changed.
458 if (xievent->event == next_xievent->event &&
459 xievent->child == next_xievent->child &&
460 xievent->detail == next_xievent->detail &&
461 xievent->buttons.mask_len == next_xievent->buttons.mask_len &&
462 (memcmp(xievent->buttons.mask,
463 next_xievent->buttons.mask,
464 xievent->buttons.mask_len) == 0) &&
465 xievent->mods.base == next_xievent->mods.base &&
466 xievent->mods.latched == next_xievent->mods.latched &&
467 xievent->mods.locked == next_xievent->mods.locked &&
468 xievent->mods.effective == next_xievent->mods.effective) {
469 XFreeEventData(display, &next_event.xcookie);
470 // Free the previous cookie.
471 if (num_coalesced > 0)
472 XFreeEventData(display, &last_event->xcookie);
473 // Get the event and its cookie data.
474 XNextEvent(display, last_event);
475 XGetEventData(display, &last_event->xcookie);
476 ++num_coalesced;
477 continue;
478 }
479 }
480 // This isn't an event we want so free its cookie data.
481 XFreeEventData(display, &next_event.xcookie);
482 break;
483 }
484
485 if (event_type == XI_Motion && num_coalesced > 0) {
486 base::TimeDelta delta = ui::EventTimeFromNative(last_event) -
487 ui::EventTimeFromNative(const_cast<XEvent*>(xev));
488 UMA_HISTOGRAM_COUNTS_10000("Event.CoalescedCount.Mouse", num_coalesced);
489 UMA_HISTOGRAM_TIMES("Event.CoalescedLatency.Mouse", delta);
490 }
491 return num_coalesced;
492 }
493
494 void HideHostCursor() {
495 CR_DEFINE_STATIC_LOCAL(XScopedCursor, invisible_cursor,
496 (CreateInvisibleCursor(), gfx::GetXDisplay()));
497 XDefineCursor(gfx::GetXDisplay(), DefaultRootWindow(gfx::GetXDisplay()),
498 invisible_cursor.get());
499 }
500
501 ::Cursor CreateInvisibleCursor() {
502 XDisplay* xdisplay = gfx::GetXDisplay();
503 ::Cursor invisible_cursor;
504 char nodata[] = { 0, 0, 0, 0, 0, 0, 0, 0 };
505 XColor black;
506 black.red = black.green = black.blue = 0;
507 Pixmap blank = XCreateBitmapFromData(xdisplay,
508 DefaultRootWindow(xdisplay),
509 nodata, 8, 8);
510 invisible_cursor = XCreatePixmapCursor(xdisplay, blank, blank,
511 &black, &black, 0, 0);
512 XFreePixmap(xdisplay, blank);
513 return invisible_cursor;
514 }
515
516 void SetUseOSWindowFrame(XID window, bool use_os_window_frame) {
517 // This data structure represents additional hints that we send to the window
518 // manager and has a direct lineage back to Motif, which defined this de facto
519 // standard. This struct doesn't seem 64-bit safe though, but it's what GDK
520 // does.
521 typedef struct {
522 unsigned long flags;
523 unsigned long functions;
524 unsigned long decorations;
525 long input_mode;
526 unsigned long status;
527 } MotifWmHints;
528
529 MotifWmHints motif_hints;
530 memset(&motif_hints, 0, sizeof(motif_hints));
531 // Signals that the reader of the _MOTIF_WM_HINTS property should pay
532 // attention to the value of |decorations|.
533 motif_hints.flags = (1L << 1);
534 motif_hints.decorations = use_os_window_frame ? 1 : 0;
535
536 XAtom hint_atom = GetAtom("_MOTIF_WM_HINTS");
537 XChangeProperty(gfx::GetXDisplay(),
538 window,
539 hint_atom,
540 hint_atom,
541 32,
542 PropModeReplace,
543 reinterpret_cast<unsigned char*>(&motif_hints),
544 sizeof(MotifWmHints)/sizeof(long));
545 }
546
547 bool IsShapeExtensionAvailable() {
548 int dummy;
549 static bool is_shape_available =
550 XShapeQueryExtension(gfx::GetXDisplay(), &dummy, &dummy);
551 return is_shape_available;
552 }
553
554 XID GetX11RootWindow() {
555 return DefaultRootWindow(gfx::GetXDisplay());
556 }
557
558 bool GetCurrentDesktop(int* desktop) {
559 return GetIntProperty(GetX11RootWindow(), "_NET_CURRENT_DESKTOP", desktop);
560 }
561
562 void SetHideTitlebarWhenMaximizedProperty(XID window,
563 HideTitlebarWhenMaximized property) {
564 // XChangeProperty() expects "hide" to be long.
565 unsigned long hide = property;
566 XChangeProperty(gfx::GetXDisplay(),
567 window,
568 GetAtom("_GTK_HIDE_TITLEBAR_WHEN_MAXIMIZED"),
569 XA_CARDINAL,
570 32, // size in bits
571 PropModeReplace,
572 reinterpret_cast<unsigned char*>(&hide),
573 1);
574 }
575
576 void ClearX11DefaultRootWindow() {
577 XDisplay* display = gfx::GetXDisplay();
578 XID root_window = GetX11RootWindow();
579 gfx::Rect root_bounds;
580 if (!GetOuterWindowBounds(root_window, &root_bounds)) {
581 LOG(ERROR) << "Failed to get the bounds of the X11 root window";
582 return;
583 }
584
585 XGCValues gc_values = {0};
586 gc_values.foreground = BlackPixel(display, DefaultScreen(display));
587 GC gc = XCreateGC(display, root_window, GCForeground, &gc_values);
588 XFillRectangle(display, root_window, gc,
589 root_bounds.x(),
590 root_bounds.y(),
591 root_bounds.width(),
592 root_bounds.height());
593 XFreeGC(display, gc);
594 }
595
596 bool IsWindowVisible(XID window) {
597 TRACE_EVENT0("ui", "IsWindowVisible");
598
599 XWindowAttributes win_attributes;
600 if (!XGetWindowAttributes(gfx::GetXDisplay(), window, &win_attributes))
601 return false;
602 if (win_attributes.map_state != IsViewable)
603 return false;
604
605 // Minimized windows are not visible.
606 std::vector<XAtom> wm_states;
607 if (GetAtomArrayProperty(window, "_NET_WM_STATE", &wm_states)) {
608 XAtom hidden_atom = GetAtom("_NET_WM_STATE_HIDDEN");
609 if (std::find(wm_states.begin(), wm_states.end(), hidden_atom) !=
610 wm_states.end()) {
611 return false;
612 }
613 }
614
615 // Some compositing window managers (notably kwin) do not actually unmap
616 // windows on desktop switch, so we also must check the current desktop.
617 int window_desktop, current_desktop;
618 return (!GetWindowDesktop(window, &window_desktop) ||
619 !GetCurrentDesktop(&current_desktop) ||
620 window_desktop == kAllDesktops ||
621 window_desktop == current_desktop);
622 }
623
624 bool GetInnerWindowBounds(XID window, gfx::Rect* rect) {
625 Window root, child;
626 int x, y;
627 unsigned int width, height;
628 unsigned int border_width, depth;
629
630 if (!XGetGeometry(gfx::GetXDisplay(), window, &root, &x, &y,
631 &width, &height, &border_width, &depth))
632 return false;
633
634 if (!XTranslateCoordinates(gfx::GetXDisplay(), window, root,
635 0, 0, &x, &y, &child))
636 return false;
637
638 *rect = gfx::Rect(x, y, width, height);
639
640 return true;
641 }
642
643 bool GetWindowExtents(XID window, gfx::Insets* extents) {
644 std::vector<int> insets;
645 if (!GetIntArrayProperty(window, "_NET_FRAME_EXTENTS", &insets))
646 return false;
647 if (insets.size() != 4)
648 return false;
649
650 int left = insets[0];
651 int right = insets[1];
652 int top = insets[2];
653 int bottom = insets[3];
654 extents->Set(-top, -left, -bottom, -right);
655 return true;
656 }
657
658 bool GetOuterWindowBounds(XID window, gfx::Rect* rect) {
659 if (!GetInnerWindowBounds(window, rect))
660 return false;
661
662 gfx::Insets extents;
663 if (GetWindowExtents(window, &extents))
664 rect->Inset(extents);
665 // Not all window managers support _NET_FRAME_EXTENTS so return true even if
666 // requesting the property fails.
667
668 return true;
669 }
670
671
672 bool WindowContainsPoint(XID window, gfx::Point screen_loc) {
673 TRACE_EVENT0("ui", "WindowContainsPoint");
674
675 gfx::Rect window_rect;
676 if (!GetOuterWindowBounds(window, &window_rect))
677 return false;
678
679 if (!window_rect.Contains(screen_loc))
680 return false;
681
682 if (!IsShapeExtensionAvailable())
683 return true;
684
685 // According to http://www.x.org/releases/X11R7.6/doc/libXext/shapelib.html,
686 // if an X display supports the shape extension the bounds of a window are
687 // defined as the intersection of the window bounds and the interior
688 // rectangles. This means to determine if a point is inside a window for the
689 // purpose of input handling we have to check the rectangles in the ShapeInput
690 // list.
691 // According to http://www.x.org/releases/current/doc/xextproto/shape.html,
692 // we need to also respect the ShapeBounding rectangles.
693 // The effective input region of a window is defined to be the intersection
694 // of the client input region with both the default input region and the
695 // client bounding region. Any portion of the client input region that is not
696 // included in both the default input region and the client bounding region
697 // will not be included in the effective input region on the screen.
698 int rectangle_kind[] = {ShapeInput, ShapeBounding};
699 for (size_t kind_index = 0;
700 kind_index < arraysize(rectangle_kind);
701 kind_index++) {
702 int dummy;
703 int shape_rects_size = 0;
704 XRectangle* shape_rects = XShapeGetRectangles(gfx::GetXDisplay(),
705 window,
706 rectangle_kind[kind_index],
707 &shape_rects_size,
708 &dummy);
709 if (!shape_rects) {
710 // The shape is empty. This can occur when |window| is minimized.
711 DCHECK_EQ(0, shape_rects_size);
712 return false;
713 }
714 bool is_in_shape_rects = false;
715 for (int i = 0; i < shape_rects_size; ++i) {
716 // The ShapeInput and ShapeBounding rects are to be in window space, so we
717 // have to translate by the window_rect's offset to map to screen space.
718 gfx::Rect shape_rect =
719 gfx::Rect(shape_rects[i].x + window_rect.x(),
720 shape_rects[i].y + window_rect.y(),
721 shape_rects[i].width, shape_rects[i].height);
722 if (shape_rect.Contains(screen_loc)) {
723 is_in_shape_rects = true;
724 break;
725 }
726 }
727 XFree(shape_rects);
728 if (!is_in_shape_rects)
729 return false;
730 }
731 return true;
732 }
733
734
735 bool PropertyExists(XID window, const std::string& property_name) {
736 XAtom type = None;
737 int format = 0; // size in bits of each item in 'property'
738 unsigned long num_items = 0;
739 unsigned char* property = NULL;
740
741 int result = GetProperty(window, property_name, 1,
742 &type, &format, &num_items, &property);
743 if (result != Success)
744 return false;
745
746 XFree(property);
747 return num_items > 0;
748 }
749
750 bool GetRawBytesOfProperty(XID window,
751 XAtom property,
752 scoped_refptr<base::RefCountedMemory>* out_data,
753 size_t* out_data_items,
754 XAtom* out_type) {
755 // Retrieve the data from our window.
756 unsigned long nitems = 0;
757 unsigned long nbytes = 0;
758 XAtom prop_type = None;
759 int prop_format = 0;
760 unsigned char* property_data = NULL;
761 if (XGetWindowProperty(gfx::GetXDisplay(), window, property,
762 0, 0x1FFFFFFF /* MAXINT32 / 4 */, False,
763 AnyPropertyType, &prop_type, &prop_format,
764 &nitems, &nbytes, &property_data) != Success) {
765 return false;
766 }
767
768 if (prop_type == None)
769 return false;
770
771 size_t bytes = 0;
772 // So even though we should theoretically have nbytes (and we can't
773 // pass NULL there), we need to manually calculate the byte length here
774 // because nbytes always returns zero.
775 switch (prop_format) {
776 case 8:
777 bytes = nitems;
778 break;
779 case 16:
780 bytes = sizeof(short) * nitems;
781 break;
782 case 32:
783 bytes = sizeof(long) * nitems;
784 break;
785 default:
786 NOTREACHED();
787 break;
788 }
789
790 if (out_data)
791 *out_data = new XRefcountedMemory(property_data, bytes);
792 else
793 XFree(property_data);
794
795 if (out_data_items)
796 *out_data_items = nitems;
797
798 if (out_type)
799 *out_type = prop_type;
800
801 return true;
802 }
803
804 bool GetIntProperty(XID window, const std::string& property_name, int* value) {
805 XAtom type = None;
806 int format = 0; // size in bits of each item in 'property'
807 unsigned long num_items = 0;
808 unsigned char* property = NULL;
809
810 int result = GetProperty(window, property_name, 1,
811 &type, &format, &num_items, &property);
812 if (result != Success)
813 return false;
814
815 if (format != 32 || num_items != 1) {
816 XFree(property);
817 return false;
818 }
819
820 *value = static_cast<int>(*(reinterpret_cast<long*>(property)));
821 XFree(property);
822 return true;
823 }
824
825 bool GetXIDProperty(XID window, const std::string& property_name, XID* value) {
826 XAtom type = None;
827 int format = 0; // size in bits of each item in 'property'
828 unsigned long num_items = 0;
829 unsigned char* property = NULL;
830
831 int result = GetProperty(window, property_name, 1,
832 &type, &format, &num_items, &property);
833 if (result != Success)
834 return false;
835
836 if (format != 32 || num_items != 1) {
837 XFree(property);
838 return false;
839 }
840
841 *value = *(reinterpret_cast<XID*>(property));
842 XFree(property);
843 return true;
844 }
845
846 bool GetIntArrayProperty(XID window,
847 const std::string& property_name,
848 std::vector<int>* value) {
849 XAtom type = None;
850 int format = 0; // size in bits of each item in 'property'
851 unsigned long num_items = 0;
852 unsigned char* properties = NULL;
853
854 int result = GetProperty(window, property_name,
855 (~0L), // (all of them)
856 &type, &format, &num_items, &properties);
857 if (result != Success)
858 return false;
859
860 if (format != 32) {
861 XFree(properties);
862 return false;
863 }
864
865 long* int_properties = reinterpret_cast<long*>(properties);
866 value->clear();
867 for (unsigned long i = 0; i < num_items; ++i) {
868 value->push_back(static_cast<int>(int_properties[i]));
869 }
870 XFree(properties);
871 return true;
872 }
873
874 bool GetAtomArrayProperty(XID window,
875 const std::string& property_name,
876 std::vector<XAtom>* value) {
877 XAtom type = None;
878 int format = 0; // size in bits of each item in 'property'
879 unsigned long num_items = 0;
880 unsigned char* properties = NULL;
881
882 int result = GetProperty(window, property_name,
883 (~0L), // (all of them)
884 &type, &format, &num_items, &properties);
885 if (result != Success)
886 return false;
887
888 if (type != XA_ATOM) {
889 XFree(properties);
890 return false;
891 }
892
893 XAtom* atom_properties = reinterpret_cast<XAtom*>(properties);
894 value->clear();
895 value->insert(value->begin(), atom_properties, atom_properties + num_items);
896 XFree(properties);
897 return true;
898 }
899
900 bool GetStringProperty(
901 XID window, const std::string& property_name, std::string* value) {
902 XAtom type = None;
903 int format = 0; // size in bits of each item in 'property'
904 unsigned long num_items = 0;
905 unsigned char* property = NULL;
906
907 int result = GetProperty(window, property_name, 1024,
908 &type, &format, &num_items, &property);
909 if (result != Success)
910 return false;
911
912 if (format != 8) {
913 XFree(property);
914 return false;
915 }
916
917 value->assign(reinterpret_cast<char*>(property), num_items);
918 XFree(property);
919 return true;
920 }
921
922 bool SetIntProperty(XID window,
923 const std::string& name,
924 const std::string& type,
925 int value) {
926 std::vector<int> values(1, value);
927 return SetIntArrayProperty(window, name, type, values);
928 }
929
930 bool SetIntArrayProperty(XID window,
931 const std::string& name,
932 const std::string& type,
933 const std::vector<int>& value) {
934 DCHECK(!value.empty());
935 XAtom name_atom = GetAtom(name.c_str());
936 XAtom type_atom = GetAtom(type.c_str());
937
938 // XChangeProperty() expects values of type 32 to be longs.
939 scoped_ptr<long[]> data(new long[value.size()]);
940 for (size_t i = 0; i < value.size(); ++i)
941 data[i] = value[i];
942
943 gfx::X11ErrorTracker err_tracker;
944 XChangeProperty(gfx::GetXDisplay(),
945 window,
946 name_atom,
947 type_atom,
948 32, // size in bits of items in 'value'
949 PropModeReplace,
950 reinterpret_cast<const unsigned char*>(data.get()),
951 value.size()); // num items
952 return !err_tracker.FoundNewError();
953 }
954
955 bool SetAtomProperty(XID window,
956 const std::string& name,
957 const std::string& type,
958 XAtom value) {
959 std::vector<XAtom> values(1, value);
960 return SetAtomArrayProperty(window, name, type, values);
961 }
962
963 bool SetAtomArrayProperty(XID window,
964 const std::string& name,
965 const std::string& type,
966 const std::vector<XAtom>& value) {
967 DCHECK(!value.empty());
968 XAtom name_atom = GetAtom(name.c_str());
969 XAtom type_atom = GetAtom(type.c_str());
970
971 // XChangeProperty() expects values of type 32 to be longs.
972 scoped_ptr<XAtom[]> data(new XAtom[value.size()]);
973 for (size_t i = 0; i < value.size(); ++i)
974 data[i] = value[i];
975
976 gfx::X11ErrorTracker err_tracker;
977 XChangeProperty(gfx::GetXDisplay(),
978 window,
979 name_atom,
980 type_atom,
981 32, // size in bits of items in 'value'
982 PropModeReplace,
983 reinterpret_cast<const unsigned char*>(data.get()),
984 value.size()); // num items
985 return !err_tracker.FoundNewError();
986 }
987
988 bool SetStringProperty(XID window,
989 XAtom property,
990 XAtom type,
991 const std::string& value) {
992 gfx::X11ErrorTracker err_tracker;
993 XChangeProperty(gfx::GetXDisplay(),
994 window,
995 property,
996 type,
997 8,
998 PropModeReplace,
999 reinterpret_cast<const unsigned char*>(value.c_str()),
1000 value.size());
1001 return !err_tracker.FoundNewError();
1002 }
1003
1004 XAtom GetAtom(const char* name) {
1005 // TODO(derat): Cache atoms to avoid round-trips to the server.
1006 return XInternAtom(gfx::GetXDisplay(), name, false);
1007 }
1008
1009 void SetWindowClassHint(XDisplay* display,
1010 XID window,
1011 const std::string& res_name,
1012 const std::string& res_class) {
1013 XClassHint class_hints;
1014 // const_cast is safe because XSetClassHint does not modify the strings.
1015 // Just to be safe, the res_name and res_class parameters are local copies,
1016 // not const references.
1017 class_hints.res_name = const_cast<char*>(res_name.c_str());
1018 class_hints.res_class = const_cast<char*>(res_class.c_str());
1019 XSetClassHint(display, window, &class_hints);
1020 }
1021
1022 void SetWindowRole(XDisplay* display, XID window, const std::string& role) {
1023 if (role.empty()) {
1024 XDeleteProperty(display, window, GetAtom("WM_WINDOW_ROLE"));
1025 } else {
1026 char* role_c = const_cast<char*>(role.c_str());
1027 XChangeProperty(display, window, GetAtom("WM_WINDOW_ROLE"), XA_STRING, 8,
1028 PropModeReplace,
1029 reinterpret_cast<unsigned char*>(role_c),
1030 role.size());
1031 }
1032 }
1033
1034 bool GetCustomFramePrefDefault() {
1035 // If the window manager doesn't support enough of EWMH to tell us its name,
1036 // assume that it doesn't want custom frames. For example, _NET_WM_MOVERESIZE
1037 // is needed for frame-drag-initiated window movement.
1038 std::string wm_name;
1039 if (!GetWindowManagerName(&wm_name))
1040 return false;
1041
1042 // Also disable custom frames for (at-least-partially-)EWMH-supporting tiling
1043 // window managers.
1044 ui::WindowManagerName wm = GuessWindowManager();
1045 if (wm == WM_AWESOME ||
1046 wm == WM_I3 ||
1047 wm == WM_ION3 ||
1048 wm == WM_MATCHBOX ||
1049 wm == WM_NOTION ||
1050 wm == WM_QTILE ||
1051 wm == WM_RATPOISON ||
1052 wm == WM_STUMPWM ||
1053 wm == WM_WMII)
1054 return false;
1055
1056 // Handle a few more window managers that don't get along well with custom
1057 // frames.
1058 if (wm == WM_ICE_WM ||
1059 wm == WM_KWIN)
1060 return false;
1061
1062 // For everything else, use custom frames.
1063 return true;
1064 }
1065
1066 bool GetWindowDesktop(XID window, int* desktop) {
1067 return GetIntProperty(window, "_NET_WM_DESKTOP", desktop);
1068 }
1069
1070 std::string GetX11ErrorString(XDisplay* display, int err) {
1071 char buffer[256];
1072 XGetErrorText(display, err, buffer, arraysize(buffer));
1073 return buffer;
1074 }
1075
1076 // Returns true if |window| is a named window.
1077 bool IsWindowNamed(XID window) {
1078 XTextProperty prop;
1079 if (!XGetWMName(gfx::GetXDisplay(), window, &prop) || !prop.value)
1080 return false;
1081
1082 XFree(prop.value);
1083 return true;
1084 }
1085
1086 bool GetXWindowStack(Window window, std::vector<XID>* windows) {
1087 windows->clear();
1088
1089 Atom type;
1090 int format;
1091 unsigned long count;
1092 unsigned char *data = NULL;
1093 if (GetProperty(window,
1094 "_NET_CLIENT_LIST_STACKING",
1095 ~0L,
1096 &type,
1097 &format,
1098 &count,
1099 &data) != Success) {
1100 return false;
1101 }
1102
1103 bool result = false;
1104 if (type == XA_WINDOW && format == 32 && data && count > 0) {
1105 result = true;
1106 XID* stack = reinterpret_cast<XID*>(data);
1107 for (long i = static_cast<long>(count) - 1; i >= 0; i--)
1108 windows->push_back(stack[i]);
1109 }
1110
1111 if (data)
1112 XFree(data);
1113
1114 return result;
1115 }
1116
1117 WindowManagerName GuessWindowManager() {
1118 std::string name;
1119 if (GetWindowManagerName(&name)) {
1120 // These names are taken from the WMs' source code.
1121 if (name == "awesome")
1122 return WM_AWESOME;
1123 if (name == "Blackbox")
1124 return WM_BLACKBOX;
1125 if (name == "Compiz" || name == "compiz")
1126 return WM_COMPIZ;
1127 if (name == "e16" || name == "Enlightenment")
1128 return WM_ENLIGHTENMENT;
1129 if (name == "i3")
1130 return WM_I3;
1131 if (StartsWithASCII(name, "IceWM", true))
1132 return WM_ICE_WM;
1133 if (name == "ion3")
1134 return WM_ION3;
1135 if (name == "KWin")
1136 return WM_KWIN;
1137 if (name == "matchbox")
1138 return WM_MATCHBOX;
1139 if (name == "Metacity")
1140 return WM_METACITY;
1141 if (name == "Mutter (Muffin)")
1142 return WM_MUFFIN;
1143 if (name == "GNOME Shell")
1144 return WM_MUTTER; // GNOME Shell uses Mutter
1145 if (name == "Mutter")
1146 return WM_MUTTER;
1147 if (name == "notion")
1148 return WM_NOTION;
1149 if (name == "Openbox")
1150 return WM_OPENBOX;
1151 if (name == "qtile")
1152 return WM_QTILE;
1153 if (name == "ratpoison")
1154 return WM_RATPOISON;
1155 if (name == "stumpwm")
1156 return WM_STUMPWM;
1157 if (name == "wmii")
1158 return WM_WMII;
1159 if (name == "Xfwm4")
1160 return WM_XFWM4;
1161 }
1162 return WM_UNKNOWN;
1163 }
1164
1165 std::string GuessWindowManagerName() {
1166 std::string name;
1167 if (GetWindowManagerName(&name))
1168 return name;
1169 return "Unknown";
1170 }
1171
1172 void SetDefaultX11ErrorHandlers() {
1173 SetX11ErrorHandlers(NULL, NULL);
1174 }
1175
1176 bool IsX11WindowFullScreen(XID window) {
1177 // If _NET_WM_STATE_FULLSCREEN is in _NET_SUPPORTED, use the presence or
1178 // absence of _NET_WM_STATE_FULLSCREEN in _NET_WM_STATE to determine
1179 // whether we're fullscreen.
1180 XAtom fullscreen_atom = GetAtom("_NET_WM_STATE_FULLSCREEN");
1181 if (WmSupportsHint(fullscreen_atom)) {
1182 std::vector<XAtom> atom_properties;
1183 if (GetAtomArrayProperty(window,
1184 "_NET_WM_STATE",
1185 &atom_properties)) {
1186 return std::find(atom_properties.begin(),
1187 atom_properties.end(),
1188 fullscreen_atom) !=
1189 atom_properties.end();
1190 }
1191 }
1192
1193 gfx::Rect window_rect;
1194 if (!ui::GetOuterWindowBounds(window, &window_rect))
1195 return false;
1196
1197 // We can't use gfx::Screen here because we don't have an aura::Window. So
1198 // instead just look at the size of the default display.
1199 //
1200 // TODO(erg): Actually doing this correctly would require pulling out xrandr,
1201 // which we don't even do in the desktop screen yet.
1202 ::XDisplay* display = gfx::GetXDisplay();
1203 ::Screen* screen = DefaultScreenOfDisplay(display);
1204 int width = WidthOfScreen(screen);
1205 int height = HeightOfScreen(screen);
1206 return window_rect.size() == gfx::Size(width, height);
1207 }
1208
1209 bool WmSupportsHint(XAtom atom) {
1210 if (!SupportsEWMH())
1211 return false;
1212
1213 std::vector<XAtom> supported_atoms;
1214 if (!GetAtomArrayProperty(GetX11RootWindow(),
1215 "_NET_SUPPORTED",
1216 &supported_atoms)) {
1217 return false;
1218 }
1219
1220 return std::find(supported_atoms.begin(), supported_atoms.end(), atom) !=
1221 supported_atoms.end();
1222 }
1223
1224 const unsigned char* XRefcountedMemory::front() const {
1225 return x11_data_;
1226 }
1227
1228 size_t XRefcountedMemory::size() const {
1229 return length_;
1230 }
1231
1232 XRefcountedMemory::~XRefcountedMemory() {
1233 XFree(x11_data_);
1234 }
1235
1236 XScopedString::~XScopedString() {
1237 XFree(string_);
1238 }
1239
1240 XScopedImage::~XScopedImage() {
1241 reset(NULL);
1242 }
1243
1244 void XScopedImage::reset(XImage* image) {
1245 if (image_ == image)
1246 return;
1247 if (image_)
1248 XDestroyImage(image_);
1249 image_ = image;
1250 }
1251
1252 XScopedCursor::XScopedCursor(::Cursor cursor, XDisplay* display)
1253 : cursor_(cursor),
1254 display_(display) {
1255 }
1256
1257 XScopedCursor::~XScopedCursor() {
1258 reset(0U);
1259 }
1260
1261 ::Cursor XScopedCursor::get() const {
1262 return cursor_;
1263 }
1264
1265 void XScopedCursor::reset(::Cursor cursor) {
1266 if (cursor_)
1267 XFreeCursor(display_, cursor_);
1268 cursor_ = cursor;
1269 }
1270
1271 namespace test {
1272
1273 void ResetXCursorCache() {
1274 delete cursor_cache;
1275 cursor_cache = NULL;
1276 }
1277
1278 const XcursorImage* GetCachedXcursorImage(::Cursor cursor) {
1279 return XCustomCursorCache::GetInstance()->GetXcursorImage(cursor);
1280 }
1281 }
1282
1283 // ----------------------------------------------------------------------------
1284 // These functions are declared in x11_util_internal.h because they require
1285 // XLib.h to be included, and it conflicts with many other headers.
1286 XRenderPictFormat* GetRenderARGB32Format(XDisplay* dpy) {
1287 static XRenderPictFormat* pictformat = NULL;
1288 if (pictformat)
1289 return pictformat;
1290
1291 // First look for a 32-bit format which ignores the alpha value
1292 XRenderPictFormat templ;
1293 templ.depth = 32;
1294 templ.type = PictTypeDirect;
1295 templ.direct.red = 16;
1296 templ.direct.green = 8;
1297 templ.direct.blue = 0;
1298 templ.direct.redMask = 0xff;
1299 templ.direct.greenMask = 0xff;
1300 templ.direct.blueMask = 0xff;
1301 templ.direct.alphaMask = 0;
1302
1303 static const unsigned long kMask =
1304 PictFormatType | PictFormatDepth |
1305 PictFormatRed | PictFormatRedMask |
1306 PictFormatGreen | PictFormatGreenMask |
1307 PictFormatBlue | PictFormatBlueMask |
1308 PictFormatAlphaMask;
1309
1310 pictformat = XRenderFindFormat(dpy, kMask, &templ, 0 /* first result */);
1311
1312 if (!pictformat) {
1313 // Not all X servers support xRGB32 formats. However, the XRENDER spec says
1314 // that they must support an ARGB32 format, so we can always return that.
1315 pictformat = XRenderFindStandardFormat(dpy, PictStandardARGB32);
1316 CHECK(pictformat) << "XRENDER ARGB32 not supported.";
1317 }
1318
1319 return pictformat;
1320 }
1321
1322 void SetX11ErrorHandlers(XErrorHandler error_handler,
1323 XIOErrorHandler io_error_handler) {
1324 XSetErrorHandler(error_handler ? error_handler : DefaultX11ErrorHandler);
1325 XSetIOErrorHandler(
1326 io_error_handler ? io_error_handler : DefaultX11IOErrorHandler);
1327 }
1328
1329 void LogErrorEventDescription(XDisplay* dpy,
1330 const XErrorEvent& error_event) {
1331 char error_str[256];
1332 char request_str[256];
1333
1334 XGetErrorText(dpy, error_event.error_code, error_str, sizeof(error_str));
1335
1336 strncpy(request_str, "Unknown", sizeof(request_str));
1337 if (error_event.request_code < 128) {
1338 std::string num = base::UintToString(error_event.request_code);
1339 XGetErrorDatabaseText(
1340 dpy, "XRequest", num.c_str(), "Unknown", request_str,
1341 sizeof(request_str));
1342 } else {
1343 int num_ext;
1344 char** ext_list = XListExtensions(dpy, &num_ext);
1345
1346 for (int i = 0; i < num_ext; i++) {
1347 int ext_code, first_event, first_error;
1348 XQueryExtension(dpy, ext_list[i], &ext_code, &first_event, &first_error);
1349 if (error_event.request_code == ext_code) {
1350 std::string msg = base::StringPrintf(
1351 "%s.%d", ext_list[i], error_event.minor_code);
1352 XGetErrorDatabaseText(
1353 dpy, "XRequest", msg.c_str(), "Unknown", request_str,
1354 sizeof(request_str));
1355 break;
1356 }
1357 }
1358 XFreeExtensionList(ext_list);
1359 }
1360
1361 LOG(WARNING)
1362 << "X error received: "
1363 << "serial " << error_event.serial << ", "
1364 << "error_code " << static_cast<int>(error_event.error_code)
1365 << " (" << error_str << "), "
1366 << "request_code " << static_cast<int>(error_event.request_code) << ", "
1367 << "minor_code " << static_cast<int>(error_event.minor_code)
1368 << " (" << request_str << ")";
1369 }
1370
1371 // ----------------------------------------------------------------------------
1372 // End of x11_util_internal.h
1373
1374
1375 } // namespace ui
OLDNEW
« no previous file with comments | « ui/base/x/x11_util.h ('k') | ui/base/x/x11_util_internal.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698