// 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 #include #include #include #include "libcef/browser/alloy/alloy_browser_host_impl.h" #include "libcef/browser/browser_host_base.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/platform_window/x11/x11_topmost_window_finder.h" #include "ui/views/widget/desktop_aura/desktop_window_tree_host_linux.h" namespace { // Restore Xlib constants that were #undef'ed by gen/ui/gfx/x/xproto.h. constexpr int CopyFromParent = 0; constexpr int InputOutput = 1; 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"; // See https://crbug.com/1066670#c57 for background. inline x11::Window ToX11Window(::Window window) { return static_cast(window); } inline ::Window ToWindow(x11::Window window) { return static_cast<::Window>(window); } // See https://crbug.com/1066670#c29 for background. inline ::Atom GetAtom(const std::string& atom_name) { return static_cast<::Atom>(gfx::GetAtom(atom_name)); } ::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(ToX11Window(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 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, nullptr, // 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] = GetAtom(kWMDeleteWindow); protocols[1] = 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_, GetAtom(kNetWMPid), XA_CARDINAL, 32, PropModeReplace, reinterpret_cast(&pid), 1); // Set the initial window name, if provided. if (!title.empty()) { XChangeProperty(xdisplay_, xwindow_, GetAtom(kNetWMName), GetAtom(kUTF8String), 8, PropModeReplace, reinterpret_cast(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 = GetAtom(kWMProtocols); ev.xclient.format = 32; ev.xclient.data.l[0] = 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 // DesktopWindowTreeHostLinux. The proxy property is referenced by // DesktopDragDropClientAuraX11::FindWindowFor. x11::Window window = x11::Window::None; ui::GetProperty(ToX11Window(toplevel_window), gfx::GetAtom(kXdndProxy), &window); ::Window proxy_target = ToWindow(window); if (proxy_target != child) { // Set the proxy target for the top-most window. XChangeProperty(xdisplay_, toplevel_window, GetAtom(kXdndProxy), XA_WINDOW, 32, PropModeReplace, reinterpret_cast(&child), 1); // Do the same for the proxy target per the spec. XChangeProperty(xdisplay_, child, GetAtom(kXdndProxy), XA_WINDOW, 32, PropModeReplace, reinterpret_cast(&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(ToX11Window(child))) { // Give focus to the child DesktopWindowTreeHostLinux. 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::DesktopWindowTreeHostLinux* CefWindowX11::GetHost() { if (browser_.get()) { ::Window child = FindChild(xdisplay_, xwindow_); if (child) { return static_cast( views::DesktopWindowTreeHostLinux::GetHostForWidget( static_cast(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(x11::Event* x11_event) { current_xevent_ = IsTargetedBy(*x11_event) ? x11_event : nullptr; } void CefWindowX11::PlatformEventDispatchFinished() { current_xevent_ = nullptr; } ui::PlatformEventDispatcher* CefWindowX11::GetPlatformEventDispatcher() { return this; } bool CefWindowX11::DispatchXEvent(x11::Event* x11_event) { if (!IsTargetedBy(*x11_event)) return false; ProcessXEvent(x11_event); 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 = GetAtom("_NET_WM_STATE"); Atom state_keep_above = 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(&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 x11::Event& x11_event) const { return ToWindow(x11_event.window()) == xwindow_; } void CefWindowX11::ProcessXEvent(x11::Event* event) { if (auto* configure = event->As()) { DCHECK_EQ(xwindow_, ToWindow(configure->event)); DCHECK_EQ(xwindow_, ToWindow(configure->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. bounds_ = gfx::Rect(configure->x, configure->y, configure->width, configure->height); if (browser_.get()) { ::Window child = FindChild(xdisplay_, xwindow_); if (child) { // Resize the child DesktopWindowTreeHostLinux to match this window. XWindowChanges changes = {0}; changes.width = bounds_.width(); changes.height = bounds_.height(); XConfigureWindow(xdisplay_, child, CWHeight | CWWidth, &changes); browser_->NotifyMoveOrResizeStarted(); } } } else if (auto* client = event->As()) { if (client->type == gfx::GetAtom(kWMProtocols)) { x11::Atom protocol = static_cast(client->data.data32[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_) { // Force the browser to be destroyed and release the reference // added in PlatformCreateWindow(). static_cast(browser_.get()) ->WindowDestroyed(); } delete this; } } else if (protocol == gfx::GetAtom(kNetWMPing)) { x11::ClientMessageEvent reply_event = *client; reply_event.window = ToX11Window(parent_xwindow_); ui::SendEvent(reply_event, reply_event.window, x11::EventMask::SubstructureNotify | x11::EventMask::SubstructureRedirect); } } } else if (auto* focus = event->As()) { if (focus->opcode == x11::FocusEvent::In) { // 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 DesktopWindowTreeHostLinux) 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); } } else { // 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; } } else if (auto* property = event->As()) { if (property->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 DesktopWindowTreeHostLinux // window so that resource usage will be reduced while the window is // minimized. std::vector atom_list; if (ui::GetAtomArrayProperty(ToX11Window(xwindow_), kNetWMState, &atom_list) && !atom_list.empty()) { ui::SetAtomArrayProperty(ToX11Window(child), kNetWMState, "ATOM", atom_list); } else { // Set an empty list of property values to pass the check in // DesktopWindowTreeHostLinux::OnWMStateUpdated(). XChangeProperty(xdisplay_, child, GetAtom(kNetWMState), // name GetAtom(kAtom), // type 32, // size in bits of items in 'value' PropModeReplace, NULL, 0); // num items } } } } } }