| // Copyright 2014 The Chromium Embedded Framework Authors. |
| // Portions 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 "libcef/browser/native/window_x11.h" |
| #include "libcef/browser/thread_util.h" |
| |
| #include "ui/base/x/x11_util.h" |
| #include "ui/events/platform/platform_event_source.h" |
| #include "ui/events/x/x11_event_translation.h" |
| #include "ui/views/widget/desktop_aura/desktop_window_tree_host_x11.h" |
| #include "ui/views/widget/desktop_aura/x11_topmost_window_finder.h" |
| |
| #include <X11/Xatom.h> |
| #include <X11/Xlib.h> |
| #include <X11/Xutil.h> |
| #include <X11/extensions/XInput2.h> |
| |
| namespace { |
| |
| const char kAtom[] = "ATOM"; |
| const char kWMDeleteWindow[] = "WM_DELETE_WINDOW"; |
| const char kWMProtocols[] = "WM_PROTOCOLS"; |
| const char kNetWMName[] = "_NET_WM_NAME"; |
| const char kNetWMPid[] = "_NET_WM_PID"; |
| const char kNetWMPing[] = "_NET_WM_PING"; |
| const char kNetWMState[] = "_NET_WM_STATE"; |
| const char kXdndProxy[] = "XdndProxy"; |
| const char kUTF8String[] = "UTF8_STRING"; |
| |
| ::Window FindChild(::Display* display, ::Window window) { |
| ::Window root; |
| ::Window parent; |
| ::Window* children; |
| ::Window child_window = x11::None; |
| unsigned int nchildren; |
| if (XQueryTree(display, window, &root, &parent, &children, &nchildren) && |
| nchildren == 1) { |
| child_window = children[0]; |
| XFree(children); |
| } |
| return child_window; |
| } |
| |
| ::Window FindToplevelParent(::Display* display, ::Window window) { |
| ::Window top_level_window = window; |
| ::Window root = x11::None; |
| ::Window parent = x11::None; |
| ::Window* children = nullptr; |
| unsigned int nchildren = 0; |
| // Enumerate all parents of "window" to find the highest level window |
| // that either: |
| // - has a parent that does not contain the _NET_WM_PID property |
| // - has a parent that is the root window. |
| while (XQueryTree(display, window, &root, &parent, &children, &nchildren)) { |
| if (children) { |
| XFree(children); |
| } |
| |
| top_level_window = window; |
| if (!ui::PropertyExists(parent, kNetWMPid) || parent == root) { |
| break; |
| } |
| window = parent; |
| } |
| return top_level_window; |
| } |
| |
| } // namespace |
| |
| CEF_EXPORT XDisplay* cef_get_xdisplay() { |
| if (!CEF_CURRENTLY_ON(CEF_UIT)) |
| return nullptr; |
| return gfx::GetXDisplay(); |
| } |
| |
| CefWindowX11::CefWindowX11(CefRefPtr<CefBrowserHostImpl> browser, |
| ::Window parent_xwindow, |
| const gfx::Rect& bounds, |
| const std::string& title) |
| : browser_(browser), |
| xdisplay_(gfx::GetXDisplay()), |
| parent_xwindow_(parent_xwindow), |
| xwindow_(0), |
| window_mapped_(false), |
| bounds_(bounds), |
| focus_pending_(false), |
| weak_ptr_factory_(this) { |
| if (parent_xwindow_ == x11::None) |
| parent_xwindow_ = DefaultRootWindow(xdisplay_); |
| |
| XSetWindowAttributes swa; |
| memset(&swa, 0, sizeof(swa)); |
| swa.background_pixmap = x11::None; |
| swa.override_redirect = false; |
| xwindow_ = XCreateWindow(xdisplay_, parent_xwindow_, bounds.x(), bounds.y(), |
| bounds.width(), bounds.height(), |
| 0, // border width |
| CopyFromParent, // depth |
| InputOutput, |
| CopyFromParent, // visual |
| CWBackPixmap | CWOverrideRedirect, &swa); |
| CHECK(xwindow_); |
| |
| DCHECK(ui::X11EventSource::HasInstance()); |
| ui::X11EventSource::GetInstance()->AddXEventDispatcher(this); |
| |
| long event_mask = FocusChangeMask | StructureNotifyMask | PropertyChangeMask; |
| XSelectInput(xdisplay_, xwindow_, event_mask); |
| XFlush(xdisplay_); |
| |
| // TODO(erg): We currently only request window deletion events. We also |
| // should listen for activation events and anything else that GTK+ listens |
| // for, and do something useful. |
| ::Atom protocols[2]; |
| protocols[0] = gfx::GetAtom(kWMDeleteWindow); |
| protocols[1] = gfx::GetAtom(kNetWMPing); |
| XSetWMProtocols(xdisplay_, xwindow_, protocols, 2); |
| |
| // We need a WM_CLIENT_MACHINE and WM_LOCALE_NAME value so we integrate with |
| // the desktop environment. |
| XSetWMProperties(xdisplay_, xwindow_, NULL, NULL, NULL, 0, NULL, NULL, NULL); |
| |
| // Likewise, the X server needs to know this window's pid so it knows which |
| // program to kill if the window hangs. |
| // XChangeProperty() expects "pid" to be long. |
| static_assert(sizeof(long) >= sizeof(pid_t), |
| "pid_t should not be larger than long"); |
| long pid = getpid(); |
| XChangeProperty(xdisplay_, xwindow_, gfx::GetAtom(kNetWMPid), XA_CARDINAL, 32, |
| PropModeReplace, reinterpret_cast<unsigned char*>(&pid), 1); |
| |
| // Set the initial window name, if provided. |
| if (!title.empty()) { |
| XChangeProperty(xdisplay_, xwindow_, gfx::GetAtom(kNetWMName), |
| gfx::GetAtom(kUTF8String), 8, PropModeReplace, |
| reinterpret_cast<const unsigned char*>(title.c_str()), |
| title.size()); |
| } |
| } |
| |
| CefWindowX11::~CefWindowX11() { |
| DCHECK(!xwindow_); |
| DCHECK(ui::X11EventSource::HasInstance()); |
| ui::X11EventSource::GetInstance()->RemoveXEventDispatcher(this); |
| } |
| |
| void CefWindowX11::Close() { |
| XEvent ev = {0}; |
| ev.xclient.type = ClientMessage; |
| ev.xclient.window = xwindow_; |
| ev.xclient.message_type = gfx::GetAtom(kWMProtocols); |
| ev.xclient.format = 32; |
| ev.xclient.data.l[0] = gfx::GetAtom(kWMDeleteWindow); |
| ev.xclient.data.l[1] = x11::CurrentTime; |
| XSendEvent(xdisplay_, xwindow_, false, NoEventMask, &ev); |
| |
| auto host = GetHost(); |
| if (host) |
| host->Close(); |
| } |
| |
| void CefWindowX11::Show() { |
| if (xwindow_ == x11::None) |
| return; |
| |
| if (!window_mapped_) { |
| // Before we map the window, set size hints. Otherwise, some window managers |
| // will ignore toplevel XMoveWindow commands. |
| XSizeHints size_hints; |
| size_hints.flags = PPosition | PWinGravity; |
| size_hints.x = bounds_.x(); |
| size_hints.y = bounds_.y(); |
| // Set StaticGravity so that the window position is not affected by the |
| // frame width when running with window manager. |
| size_hints.win_gravity = StaticGravity; |
| XSetWMNormalHints(xdisplay_, xwindow_, &size_hints); |
| |
| XMapWindow(xdisplay_, xwindow_); |
| |
| // TODO(thomasanderson): Find out why this flush is necessary. |
| XFlush(xdisplay_); |
| window_mapped_ = true; |
| |
| // Setup the drag and drop proxy on the top level window of the application |
| // to be the child of this window. |
| ::Window child = FindChild(xdisplay_, xwindow_); |
| ::Window toplevel_window = FindToplevelParent(xdisplay_, xwindow_); |
| DCHECK(toplevel_window); |
| if (child && toplevel_window) { |
| // Configure the drag&drop proxy property for the top-most window so |
| // that all drag&drop-related messages will be sent to the child |
| // DesktopWindowTreeHostX11. The proxy property is referenced by |
| // DesktopDragDropClientAuraX11::FindWindowFor. |
| ::Window proxy_target = gfx::kNullAcceleratedWidget; |
| ui::GetXIDProperty(toplevel_window, kXdndProxy, &proxy_target); |
| |
| if (proxy_target != child) { |
| // Set the proxy target for the top-most window. |
| XChangeProperty(xdisplay_, toplevel_window, gfx::GetAtom(kXdndProxy), |
| XA_WINDOW, 32, PropModeReplace, |
| reinterpret_cast<unsigned char*>(&child), 1); |
| // Do the same for the proxy target per the spec. |
| XChangeProperty(xdisplay_, child, gfx::GetAtom(kXdndProxy), XA_WINDOW, |
| 32, PropModeReplace, |
| reinterpret_cast<unsigned char*>(&child), 1); |
| } |
| } |
| } |
| } |
| |
| void CefWindowX11::Hide() { |
| if (xwindow_ == x11::None) |
| return; |
| |
| if (window_mapped_) { |
| XWithdrawWindow(xdisplay_, xwindow_, 0); |
| window_mapped_ = false; |
| } |
| } |
| |
| void CefWindowX11::Focus() { |
| if (xwindow_ == x11::None || !window_mapped_) |
| return; |
| |
| if (browser_.get()) { |
| ::Window child = FindChild(xdisplay_, xwindow_); |
| if (child && ui::IsWindowVisible(child)) { |
| // Give focus to the child DesktopWindowTreeHostX11. |
| XSetInputFocus(xdisplay_, child, RevertToParent, x11::CurrentTime); |
| } |
| } else { |
| XSetInputFocus(xdisplay_, xwindow_, RevertToParent, x11::CurrentTime); |
| } |
| } |
| |
| void CefWindowX11::SetBounds(const gfx::Rect& bounds) { |
| if (xwindow_ == x11::None) |
| return; |
| |
| bool origin_changed = bounds_.origin() != bounds.origin(); |
| bool size_changed = bounds_.size() != bounds.size(); |
| XWindowChanges changes = {0}; |
| unsigned value_mask = 0; |
| |
| if (size_changed) { |
| changes.width = bounds.width(); |
| changes.height = bounds.height(); |
| value_mask = CWHeight | CWWidth; |
| } |
| |
| if (origin_changed) { |
| changes.x = bounds.x(); |
| changes.y = bounds.y(); |
| value_mask |= CWX | CWY; |
| } |
| |
| if (value_mask) |
| XConfigureWindow(xdisplay_, xwindow_, value_mask, &changes); |
| } |
| |
| gfx::Rect CefWindowX11::GetBoundsInScreen() { |
| int x, y; |
| Window child; |
| if (XTranslateCoordinates(xdisplay_, xwindow_, DefaultRootWindow(xdisplay_), |
| 0, 0, &x, &y, &child)) { |
| return gfx::Rect(gfx::Point(x, y), bounds_.size()); |
| } |
| return gfx::Rect(); |
| } |
| |
| views::DesktopWindowTreeHostX11* CefWindowX11::GetHost() { |
| if (browser_.get()) { |
| ::Window child = FindChild(xdisplay_, xwindow_); |
| if (child) { |
| return static_cast<views::DesktopWindowTreeHostX11*>( |
| views::DesktopWindowTreeHostLinux::GetHostForWidget(child)); |
| } |
| } |
| return nullptr; |
| } |
| |
| bool CefWindowX11::CanDispatchEvent(const ui::PlatformEvent& event) { |
| DCHECK_NE(xwindow_, x11::None); |
| return !!current_xevent_; |
| } |
| |
| uint32_t CefWindowX11::DispatchEvent(const ui::PlatformEvent& event) { |
| DCHECK_NE(xwindow_, x11::None); |
| DCHECK(event); |
| DCHECK(current_xevent_); |
| |
| ProcessXEvent(current_xevent_); |
| return ui::POST_DISPATCH_STOP_PROPAGATION; |
| } |
| |
| // Called by X11EventSourceLibevent to determine whether this XEventDispatcher |
| // implementation is able to process the next translated event sent by it. |
| void CefWindowX11::CheckCanDispatchNextPlatformEvent(XEvent* xev) { |
| current_xevent_ = IsTargetedBy(*xev) ? xev : nullptr; |
| } |
| |
| void CefWindowX11::PlatformEventDispatchFinished() { |
| current_xevent_ = nullptr; |
| } |
| |
| ui::PlatformEventDispatcher* CefWindowX11::GetPlatformEventDispatcher() { |
| return this; |
| } |
| |
| bool CefWindowX11::DispatchXEvent(XEvent* xev) { |
| if (!IsTargetedBy(*xev)) |
| return false; |
| ProcessXEvent(xev); |
| return true; |
| } |
| |
| void CefWindowX11::ContinueFocus() { |
| if (!focus_pending_) |
| return; |
| if (browser_.get()) |
| browser_->SetFocus(true); |
| focus_pending_ = false; |
| } |
| |
| bool CefWindowX11::TopLevelAlwaysOnTop() const { |
| ::Window toplevel_window = FindToplevelParent(xdisplay_, xwindow_); |
| |
| Atom state_atom = gfx::GetAtom("_NET_WM_STATE"); |
| Atom state_keep_above = gfx::GetAtom("_NET_WM_STATE_KEEP_ABOVE"); |
| Atom* states; |
| |
| Atom actual_type; |
| int actual_format; |
| unsigned long num_items; |
| unsigned long bytes_after; |
| |
| XGetWindowProperty(xdisplay_, toplevel_window, state_atom, 0, 1024, |
| x11::False, XA_ATOM, &actual_type, &actual_format, |
| &num_items, &bytes_after, |
| reinterpret_cast<unsigned char**>(&states)); |
| |
| bool always_on_top = false; |
| |
| for (unsigned long i = 0; i < num_items; ++i) { |
| if (states[i] == state_keep_above) { |
| always_on_top = true; |
| break; |
| } |
| } |
| |
| XFree(states); |
| |
| return always_on_top; |
| } |
| |
| bool CefWindowX11::IsTargetedBy(const XEvent& xev) const { |
| ::Window target_window = |
| (xev.type == GenericEvent) |
| ? static_cast<XIDeviceEvent*>(xev.xcookie.data)->event |
| : xev.xany.window; |
| return target_window == xwindow_; |
| } |
| |
| void CefWindowX11::ProcessXEvent(XEvent* xev) { |
| switch (xev->type) { |
| case ConfigureNotify: { |
| DCHECK_EQ(xwindow_, xev->xconfigure.event); |
| DCHECK_EQ(xwindow_, xev->xconfigure.window); |
| // It's possible that the X window may be resized by some other means |
| // than from within Aura (e.g. the X window manager can change the |
| // size). Make sure the root window size is maintained properly. |
| gfx::Rect bounds(xev->xconfigure.x, xev->xconfigure.y, |
| xev->xconfigure.width, xev->xconfigure.height); |
| bounds_ = bounds; |
| |
| if (browser_.get()) { |
| ::Window child = FindChild(xdisplay_, xwindow_); |
| if (child) { |
| // Resize the child DesktopWindowTreeHostX11 to match this window. |
| XWindowChanges changes = {0}; |
| changes.width = bounds.width(); |
| changes.height = bounds.height(); |
| XConfigureWindow(xdisplay_, child, CWHeight | CWWidth, &changes); |
| |
| browser_->NotifyMoveOrResizeStarted(); |
| } |
| } |
| break; |
| } |
| case ClientMessage: { |
| Atom message_type = xev->xclient.message_type; |
| if (message_type == gfx::GetAtom(kWMProtocols)) { |
| Atom protocol = static_cast<Atom>(xev->xclient.data.l[0]); |
| if (protocol == gfx::GetAtom(kWMDeleteWindow)) { |
| // We have received a close message from the window manager. |
| if (!browser_ || browser_->TryCloseBrowser()) { |
| // Allow the close. |
| XDestroyWindow(xdisplay_, xwindow_); |
| |
| xwindow_ = x11::None; |
| |
| if (browser_.get()) { |
| // Force the browser to be destroyed and release the reference |
| // added in PlatformCreateWindow(). |
| browser_->WindowDestroyed(); |
| } |
| |
| delete this; |
| } |
| } else if (protocol == gfx::GetAtom(kNetWMPing)) { |
| XEvent reply_event = *xev; |
| reply_event.xclient.window = parent_xwindow_; |
| |
| XSendEvent(xdisplay_, reply_event.xclient.window, false, |
| SubstructureRedirectMask | SubstructureNotifyMask, |
| &reply_event); |
| XFlush(xdisplay_); |
| } |
| } |
| break; |
| } |
| case x11::FocusIn: |
| // This message is received first followed by a "_NET_ACTIVE_WINDOW" |
| // message sent to the root window. When X11DesktopHandler handles the |
| // "_NET_ACTIVE_WINDOW" message it will erroneously mark the WebView |
| // (hosted in a DesktopWindowTreeHostX11) as unfocused. Use a delayed |
| // task here to restore the WebView's focus state. |
| if (!focus_pending_) { |
| focus_pending_ = true; |
| CEF_POST_DELAYED_TASK(CEF_UIT, |
| base::Bind(&CefWindowX11::ContinueFocus, |
| weak_ptr_factory_.GetWeakPtr()), |
| 100); |
| } |
| break; |
| case x11::FocusOut: |
| // Cancel the pending focus change if some other window has gained focus |
| // while waiting for the async task to run. Otherwise we can get stuck in |
| // a focus change loop. |
| if (focus_pending_) |
| focus_pending_ = false; |
| break; |
| case PropertyNotify: { |
| ::Atom changed_atom = xev->xproperty.atom; |
| if (changed_atom == gfx::GetAtom(kNetWMState)) { |
| // State change event like minimize/maximize. |
| if (browser_.get()) { |
| ::Window child = FindChild(xdisplay_, xwindow_); |
| if (child) { |
| // Forward the state change to the child DesktopWindowTreeHostX11 |
| // window so that resource usage will be reduced while the window is |
| // minimized. |
| std::vector<::Atom> atom_list; |
| if (ui::GetAtomArrayProperty(xwindow_, kNetWMState, &atom_list) && |
| !atom_list.empty()) { |
| ui::SetAtomArrayProperty(child, kNetWMState, "ATOM", atom_list); |
| } else { |
| // Set an empty list of property values to pass the check in |
| // DesktopWindowTreeHostX11::OnWMStateUpdated(). |
| XChangeProperty(xdisplay_, child, |
| gfx::GetAtom(kNetWMState), // name |
| gfx::GetAtom(kAtom), // type |
| 32, // size in bits of items in 'value' |
| PropModeReplace, NULL, |
| 0); // num items |
| } |
| } |
| } |
| } |
| break; |
| } |
| } |
| } |