| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/views/widget/desktop_aura/x11_topmost_window_finder.h" |
| |
| #include <algorithm> |
| #include <vector> |
| #include <X11/extensions/shape.h> |
| #include <X11/Xlib.h> |
| #include <X11/Xregion.h> |
| |
| // Get rid of X11 macros which conflict with gtest. |
| #undef Bool |
| #undef None |
| |
| #include "base/memory/scoped_ptr.h" |
| #include "third_party/skia/include/core/SkRect.h" |
| #include "third_party/skia/include/core/SkRegion.h" |
| #include "ui/aura/window.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/events/platform/x11/x11_event_source.h" |
| #include "ui/gfx/path.h" |
| #include "ui/gfx/path_x11.h" |
| #include "ui/gfx/x/x11_atom_cache.h" |
| #include "ui/views/test/views_test_base.h" |
| #include "ui/views/test/x11_property_change_waiter.h" |
| #include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h" |
| #include "ui/views/widget/desktop_aura/x11_desktop_handler.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace views { |
| |
| namespace { |
| |
| // Waits till |window| is minimized. |
| class MinimizeWaiter : public X11PropertyChangeWaiter { |
| public: |
| explicit MinimizeWaiter(XID window) |
| : X11PropertyChangeWaiter(window, "_NET_WM_STATE") { |
| const char* kAtomsToCache[] = { "_NET_WM_STATE_HIDDEN", NULL }; |
| atom_cache_.reset(new ui::X11AtomCache(gfx::GetXDisplay(), kAtomsToCache)); |
| } |
| |
| virtual ~MinimizeWaiter() { |
| } |
| |
| private: |
| // X11PropertyChangeWaiter: |
| virtual bool ShouldKeepOnWaiting(const ui::PlatformEvent& event) OVERRIDE { |
| std::vector<Atom> wm_states; |
| if (ui::GetAtomArrayProperty(xwindow(), "_NET_WM_STATE", &wm_states)) { |
| std::vector<Atom>::iterator it = std::find( |
| wm_states.begin(), |
| wm_states.end(), |
| atom_cache_->GetAtom("_NET_WM_STATE_HIDDEN")); |
| return it == wm_states.end(); |
| } |
| return true; |
| } |
| |
| scoped_ptr<ui::X11AtomCache> atom_cache_; |
| |
| DISALLOW_COPY_AND_ASSIGN(MinimizeWaiter); |
| }; |
| |
| // Waits till |_NET_CLIENT_LIST_STACKING| is updated to include |
| // |expected_windows|. |
| class StackingClientListWaiter : public X11PropertyChangeWaiter { |
| public: |
| StackingClientListWaiter(XID* expected_windows, size_t count) |
| : X11PropertyChangeWaiter(ui::GetX11RootWindow(), |
| "_NET_CLIENT_LIST_STACKING"), |
| expected_windows_(expected_windows, expected_windows + count) { |
| } |
| |
| virtual ~StackingClientListWaiter() { |
| } |
| |
| // X11PropertyChangeWaiter: |
| virtual void Wait() OVERRIDE { |
| // StackingClientListWaiter may be created after |
| // _NET_CLIENT_LIST_STACKING already contains |expected_windows|. |
| if (!ShouldKeepOnWaiting(NULL)) |
| return; |
| |
| X11PropertyChangeWaiter::Wait(); |
| } |
| |
| private: |
| // X11PropertyChangeWaiter: |
| virtual bool ShouldKeepOnWaiting(const ui::PlatformEvent& event) OVERRIDE { |
| std::vector<XID> stack; |
| ui::GetXWindowStack(ui::GetX11RootWindow(), &stack); |
| for (size_t i = 0; i < expected_windows_.size(); ++i) { |
| std::vector<XID>::iterator it = std::find( |
| stack.begin(), stack.end(), expected_windows_[i]); |
| if (it == stack.end()) |
| return true; |
| } |
| return false; |
| } |
| |
| std::vector<XID> expected_windows_; |
| |
| DISALLOW_COPY_AND_ASSIGN(StackingClientListWaiter); |
| }; |
| |
| } // namespace |
| |
| class X11TopmostWindowFinderTest : public ViewsTestBase { |
| public: |
| X11TopmostWindowFinderTest() { |
| } |
| |
| virtual ~X11TopmostWindowFinderTest() { |
| } |
| |
| // Creates and shows a Widget with |bounds|. The caller takes ownership of |
| // the returned widget. |
| scoped_ptr<Widget> CreateAndShowWidget(const gfx::Rect& bounds) { |
| scoped_ptr<Widget> toplevel(new Widget); |
| Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW); |
| params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| params.native_widget = new DesktopNativeWidgetAura(toplevel.get()); |
| params.bounds = bounds; |
| params.remove_standard_frame = true; |
| toplevel->Init(params); |
| toplevel->Show(); |
| return toplevel.Pass(); |
| } |
| |
| // Creates and shows an X window with |bounds|. |
| XID CreateAndShowXWindow(const gfx::Rect& bounds) { |
| XID root = DefaultRootWindow(xdisplay()); |
| XID xid = XCreateSimpleWindow(xdisplay(), |
| root, |
| 0, 0, 1, 1, |
| 0, // border_width |
| 0, // border |
| 0); // background |
| |
| ui::SetUseOSWindowFrame(xid, false); |
| ShowAndSetXWindowBounds(xid, bounds); |
| return xid; |
| } |
| |
| // Shows |xid| and sets its bounds. |
| void ShowAndSetXWindowBounds(XID xid, const gfx::Rect& bounds) { |
| XMapWindow(xdisplay(), xid); |
| |
| XWindowChanges changes = {0}; |
| changes.x = bounds.x(); |
| changes.y = bounds.y(); |
| changes.width = bounds.width(); |
| changes.height = bounds.height(); |
| XConfigureWindow(xdisplay(), |
| xid, |
| CWX | CWY | CWWidth | CWHeight, |
| &changes); |
| } |
| |
| Display* xdisplay() { |
| return gfx::GetXDisplay(); |
| } |
| |
| // Returns the topmost X window at the passed in screen position. |
| XID FindTopmostXWindowAt(int screen_x, int screen_y) { |
| X11TopmostWindowFinder finder; |
| return finder.FindWindowAt(gfx::Point(screen_x, screen_y)); |
| } |
| |
| // Returns the topmost aura::Window at the passed in screen position. Returns |
| // NULL if the topmost window does not have an associated aura::Window. |
| aura::Window* FindTopmostLocalProcessWindowAt(int screen_x, int screen_y) { |
| X11TopmostWindowFinder finder; |
| return finder.FindLocalProcessWindowAt(gfx::Point(screen_x, screen_y), |
| std::set<aura::Window*>()); |
| } |
| |
| // Returns the topmost aura::Window at the passed in screen position ignoring |
| // |ignore_window|. Returns NULL if the topmost window does not have an |
| // associated aura::Window. |
| aura::Window* FindTopmostLocalProcessWindowWithIgnore( |
| int screen_x, |
| int screen_y, |
| aura::Window* ignore_window) { |
| std::set<aura::Window*> ignore; |
| ignore.insert(ignore_window); |
| X11TopmostWindowFinder finder; |
| return finder.FindLocalProcessWindowAt(gfx::Point(screen_x, screen_y), |
| ignore); |
| } |
| |
| // ViewsTestBase: |
| virtual void SetUp() OVERRIDE { |
| ViewsTestBase::SetUp(); |
| |
| // Make X11 synchronous for our display connection. This does not force the |
| // window manager to behave synchronously. |
| XSynchronize(xdisplay(), True); |
| |
| // Ensure that the X11DesktopHandler exists. The X11DesktopHandler is |
| // necessary to properly track menu windows. |
| X11DesktopHandler::get(); |
| } |
| |
| virtual void TearDown() OVERRIDE { |
| XSynchronize(xdisplay(), False); |
| ViewsTestBase::TearDown(); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(X11TopmostWindowFinderTest); |
| }; |
| |
| TEST_F(X11TopmostWindowFinderTest, Basic) { |
| // Avoid positioning test windows at 0x0 because window managers often have a |
| // panel/launcher along one of the screen edges and do not allow windows to |
| // position themselves to overlap the panel/launcher. |
| scoped_ptr<Widget> widget1( |
| CreateAndShowWidget(gfx::Rect(100, 100, 200, 100))); |
| aura::Window* window1 = widget1->GetNativeWindow(); |
| XID xid1 = window1->GetHost()->GetAcceleratedWidget(); |
| |
| XID xid2 = CreateAndShowXWindow(gfx::Rect(200, 100, 100, 200)); |
| |
| scoped_ptr<Widget> widget3( |
| CreateAndShowWidget(gfx::Rect(100, 190, 200, 110))); |
| aura::Window* window3 = widget3->GetNativeWindow(); |
| XID xid3 = window3->GetHost()->GetAcceleratedWidget(); |
| |
| XID xids[] = { xid1, xid2, xid3 }; |
| StackingClientListWaiter waiter(xids, arraysize(xids)); |
| waiter.Wait(); |
| ui::X11EventSource::GetInstance()->DispatchXEvents(); |
| |
| EXPECT_EQ(xid1, FindTopmostXWindowAt(150, 150)); |
| EXPECT_EQ(window1, FindTopmostLocalProcessWindowAt(150, 150)); |
| |
| EXPECT_EQ(xid2, FindTopmostXWindowAt(250, 150)); |
| EXPECT_EQ(NULL, FindTopmostLocalProcessWindowAt(250, 150)); |
| |
| EXPECT_EQ(xid3, FindTopmostXWindowAt(250, 250)); |
| EXPECT_EQ(window3, FindTopmostLocalProcessWindowAt(250, 250)); |
| |
| EXPECT_EQ(xid3, FindTopmostXWindowAt(150, 250)); |
| EXPECT_EQ(window3, FindTopmostLocalProcessWindowAt(150, 250)); |
| |
| EXPECT_EQ(xid3, FindTopmostXWindowAt(150, 195)); |
| EXPECT_EQ(window3, FindTopmostLocalProcessWindowAt(150, 195)); |
| |
| EXPECT_NE(xid1, FindTopmostXWindowAt(1000, 1000)); |
| EXPECT_NE(xid2, FindTopmostXWindowAt(1000, 1000)); |
| EXPECT_NE(xid3, FindTopmostXWindowAt(1000, 1000)); |
| EXPECT_EQ(NULL, FindTopmostLocalProcessWindowAt(1000, 1000)); |
| |
| EXPECT_EQ(window1, |
| FindTopmostLocalProcessWindowWithIgnore(150, 150, window3)); |
| EXPECT_EQ(NULL, |
| FindTopmostLocalProcessWindowWithIgnore(250, 250, window3)); |
| EXPECT_EQ(NULL, |
| FindTopmostLocalProcessWindowWithIgnore(150, 250, window3)); |
| EXPECT_EQ(window1, |
| FindTopmostLocalProcessWindowWithIgnore(150, 195, window3)); |
| |
| XDestroyWindow(xdisplay(), xid2); |
| } |
| |
| // Test that the minimized state is properly handled. |
| TEST_F(X11TopmostWindowFinderTest, Minimized) { |
| scoped_ptr<Widget> widget1( |
| CreateAndShowWidget(gfx::Rect(100, 100, 100, 100))); |
| aura::Window* window1 = widget1->GetNativeWindow(); |
| XID xid1 = window1->GetHost()->GetAcceleratedWidget(); |
| XID xid2 = CreateAndShowXWindow(gfx::Rect(300, 100, 100, 100)); |
| |
| XID xids[] = { xid1, xid2 }; |
| StackingClientListWaiter stack_waiter(xids, arraysize(xids)); |
| stack_waiter.Wait(); |
| ui::X11EventSource::GetInstance()->DispatchXEvents(); |
| |
| EXPECT_EQ(xid1, FindTopmostXWindowAt(150, 150)); |
| { |
| MinimizeWaiter minimize_waiter(xid1); |
| XIconifyWindow(xdisplay(), xid1, 0); |
| minimize_waiter.Wait(); |
| } |
| EXPECT_NE(xid1, FindTopmostXWindowAt(150, 150)); |
| EXPECT_NE(xid2, FindTopmostXWindowAt(150, 150)); |
| |
| // Repeat test for an X window which does not belong to a views::Widget |
| // because the code path is different. |
| EXPECT_EQ(xid2, FindTopmostXWindowAt(350, 150)); |
| { |
| MinimizeWaiter minimize_waiter(xid2); |
| XIconifyWindow(xdisplay(), xid2, 0); |
| minimize_waiter.Wait(); |
| } |
| EXPECT_NE(xid1, FindTopmostXWindowAt(350, 150)); |
| EXPECT_NE(xid2, FindTopmostXWindowAt(350, 150)); |
| |
| XDestroyWindow(xdisplay(), xid2); |
| } |
| |
| // Test that non-rectangular windows are properly handled. |
| TEST_F(X11TopmostWindowFinderTest, NonRectangular) { |
| if (!ui::IsShapeExtensionAvailable()) |
| return; |
| |
| scoped_ptr<Widget> widget1( |
| CreateAndShowWidget(gfx::Rect(100, 100, 100, 100))); |
| XID xid1 = widget1->GetNativeWindow()->GetHost()->GetAcceleratedWidget(); |
| SkRegion* skregion1 = new SkRegion; |
| skregion1->op(SkIRect::MakeXYWH(0, 10, 10, 90), SkRegion::kUnion_Op); |
| skregion1->op(SkIRect::MakeXYWH(10, 0, 90, 100), SkRegion::kUnion_Op); |
| // Widget takes ownership of |skregion1|. |
| widget1->SetShape(skregion1); |
| |
| SkRegion skregion2; |
| skregion2.op(SkIRect::MakeXYWH(0, 10, 10, 90), SkRegion::kUnion_Op); |
| skregion2.op(SkIRect::MakeXYWH(10, 0, 90, 100), SkRegion::kUnion_Op); |
| XID xid2 = CreateAndShowXWindow(gfx::Rect(300, 100, 100, 100)); |
| REGION* region2 = gfx::CreateRegionFromSkRegion(skregion2); |
| XShapeCombineRegion(xdisplay(), xid2, ShapeBounding, 0, 0, region2, |
| false); |
| XDestroyRegion(region2); |
| |
| XID xids[] = { xid1, xid2 }; |
| StackingClientListWaiter stack_waiter(xids, arraysize(xids)); |
| stack_waiter.Wait(); |
| ui::X11EventSource::GetInstance()->DispatchXEvents(); |
| |
| EXPECT_EQ(xid1, FindTopmostXWindowAt(105, 120)); |
| EXPECT_NE(xid1, FindTopmostXWindowAt(105, 105)); |
| EXPECT_NE(xid2, FindTopmostXWindowAt(105, 105)); |
| |
| // Repeat test for an X window which does not belong to a views::Widget |
| // because the code path is different. |
| EXPECT_EQ(xid2, FindTopmostXWindowAt(305, 120)); |
| EXPECT_NE(xid1, FindTopmostXWindowAt(305, 105)); |
| EXPECT_NE(xid2, FindTopmostXWindowAt(305, 105)); |
| |
| XDestroyWindow(xdisplay(), xid2); |
| } |
| |
| // Test that the TopmostWindowFinder finds windows which belong to menus |
| // (which may or may not belong to Chrome). |
| TEST_F(X11TopmostWindowFinderTest, Menu) { |
| XID xid = CreateAndShowXWindow(gfx::Rect(100, 100, 100, 100)); |
| |
| XID root = DefaultRootWindow(xdisplay()); |
| XSetWindowAttributes swa; |
| swa.override_redirect = True; |
| XID menu_xid = XCreateWindow(xdisplay(), |
| root, |
| 0, 0, 1, 1, |
| 0, // border width |
| CopyFromParent, // depth |
| InputOutput, |
| CopyFromParent, // visual |
| CWOverrideRedirect, |
| &swa); |
| { |
| const char* kAtomsToCache[] = { "_NET_WM_WINDOW_TYPE_MENU", NULL }; |
| ui::X11AtomCache atom_cache(gfx::GetXDisplay(), kAtomsToCache); |
| ui::SetAtomProperty(menu_xid, |
| "_NET_WM_WINDOW_TYPE", |
| "ATOM", |
| atom_cache.GetAtom("_NET_WM_WINDOW_TYPE_MENU")); |
| } |
| ui::SetUseOSWindowFrame(menu_xid, false); |
| ShowAndSetXWindowBounds(menu_xid, gfx::Rect(140, 110, 100, 100)); |
| ui::X11EventSource::GetInstance()->DispatchXEvents(); |
| |
| // |menu_xid| is never added to _NET_CLIENT_LIST_STACKING. |
| XID xids[] = { xid }; |
| StackingClientListWaiter stack_waiter(xids, arraysize(xids)); |
| stack_waiter.Wait(); |
| |
| EXPECT_EQ(xid, FindTopmostXWindowAt(110, 110)); |
| EXPECT_EQ(menu_xid, FindTopmostXWindowAt(150, 120)); |
| EXPECT_EQ(menu_xid, FindTopmostXWindowAt(210, 120)); |
| |
| XDestroyWindow(xdisplay(), xid); |
| XDestroyWindow(xdisplay(), menu_xid); |
| } |
| |
| } // namespace views |