x11_whole_screen_move_loop.cc revision 5c02ac1a9c1b504631c0a3d2b6e737b5d738bae1
190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)// Copyright (c) 2012 The Chromium Authors. All rights reserved. 290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be 390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)// found in the LICENSE file. 490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#include "ui/views/widget/desktop_aura/x11_whole_screen_move_loop.h" 690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#include <X11/Xlib.h> 890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)// Get rid of a macro from Xlib.h that conflicts with Aura's RootWindow class. 990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#undef RootWindow 1090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 11effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "base/bind.h" 127d4cd473f85ac64c3747c96c277f9e506a0d2246Torne (Richard Coles)#include "base/message_loop/message_loop.h" 1390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#include "base/run_loop.h" 14a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)#include "third_party/skia/include/core/SkBitmap.h" 1590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#include "ui/aura/env.h" 16f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)#include "ui/aura/window.h" 17a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)#include "ui/aura/window_event_dispatcher.h" 18f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)#include "ui/aura/window_tree_host.h" 1990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#include "ui/base/x/x11_util.h" 20d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)#include "ui/events/event.h" 215c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu#include "ui/events/event_utils.h" 22a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)#include "ui/events/keycodes/keyboard_code_conversion_x.h" 235c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu#include "ui/events/platform/scoped_event_dispatcher.h" 24c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch#include "ui/events/platform/x11/x11_event_source.h" 25f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)#include "ui/gfx/point_conversions.h" 2690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#include "ui/gfx/screen.h" 27f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)#include "ui/views/controls/image_view.h" 28f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)#include "ui/views/widget/widget.h" 2990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 3090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)namespace views { 3190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 32f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)namespace { 33f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) 34a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)// The minimum alpha before we declare a pixel transparent when searching in 35a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)// our source image. 36a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)const uint32 kMinAlpha = 32; 37effb81e5f8246d0db0270817048dc992db66e9fbBen Murdochconst unsigned char kDragWidgetOpacity = 0xc0; 38a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 39f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)class ScopedCapturer { 40f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) public: 415d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) explicit ScopedCapturer(aura::WindowTreeHost* host) 42f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) : host_(host) { 43f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) host_->SetCapture(); 44f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) } 45f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) 46f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) ~ScopedCapturer() { 47f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) host_->ReleaseCapture(); 48f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) } 49f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) 50f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) private: 515d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) aura::WindowTreeHost* host_; 52f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) 53f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) DISALLOW_COPY_AND_ASSIGN(ScopedCapturer); 54f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)}; 55f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) 56f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)} // namespace 57f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) 5890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)X11WholeScreenMoveLoop::X11WholeScreenMoveLoop( 5990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) X11WholeScreenMoveLoopDelegate* delegate) 6090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) : delegate_(delegate), 6190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) in_move_loop_(false), 625d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) should_reset_mouse_flags_(false), 63effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch grab_input_window_(None), 64c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch canceled_(false), 65effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch weak_factory_(this) { 66effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch last_xmotion_.type = LASTEvent; 6790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)} 6890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 6990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)X11WholeScreenMoveLoop::~X11WholeScreenMoveLoop() {} 7090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 71effb81e5f8246d0db0270817048dc992db66e9fbBen Murdochvoid X11WholeScreenMoveLoop::DispatchMouseMovement() { 72effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch if (!weak_factory_.HasWeakPtrs()) 73effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch return; 74effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch weak_factory_.InvalidateWeakPtrs(); 75effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch DCHECK_EQ(MotionNotify, last_xmotion_.type); 76effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch delegate_->OnMouseMovement(&last_xmotion_); 77effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch last_xmotion_.type = LASTEvent; 78effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch} 79effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch 80c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch//////////////////////////////////////////////////////////////////////////////// 81c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch// DesktopWindowTreeHostLinux, ui::PlatformEventDispatcher implementation: 82c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch 83c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochbool X11WholeScreenMoveLoop::CanDispatchEvent(const ui::PlatformEvent& event) { 845c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return in_move_loop_; 85c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch} 86c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch 87c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochuint32_t X11WholeScreenMoveLoop::DispatchEvent(const ui::PlatformEvent& event) { 885c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu // This method processes all events for the grab_input_window_ as well as 895c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu // mouse events for all windows while the move loop is active - even before 905c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu // the grab is granted by X. This allows mouse notification events that were 915c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu // sent after the capture was requested but before the capture was granted 925c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu // to be dispatched. It is especially important to process the mouse release 935c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu // event that should have stopped the drag even if that mouse release happened 945c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu // before the grab was granted. 955c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu if (!in_move_loop_) 965c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return ui::POST_DISPATCH_PERFORM_DEFAULT; 9790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) XEvent* xev = event; 9890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 9990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) // Note: the escape key is handled in the tab drag controller, which has 10090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) // keyboard focus even though we took pointer grab. 10190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) switch (xev->type) { 10290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) case MotionNotify: { 103f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) if (drag_widget_.get()) { 104f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) gfx::Screen* screen = gfx::Screen::GetNativeScreen(); 105f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) gfx::Point location = gfx::ToFlooredPoint( 106f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) screen->GetCursorScreenPoint() - drag_offset_); 107f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) drag_widget_->SetBounds(gfx::Rect(location, drag_image_.size())); 108effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch drag_widget_->StackAtTop(); 109effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch } 110effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch last_xmotion_ = xev->xmotion; 111effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch if (!weak_factory_.HasWeakPtrs()) { 112effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch // Post a task to dispatch mouse movement event when control returns to 113effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch // the message loop. This allows smoother dragging since the events are 114effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch // dispatched without waiting for the drag widget updates. 115effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch base::MessageLoopForUI::current()->PostTask( 116effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch FROM_HERE, 117effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch base::Bind(&X11WholeScreenMoveLoop::DispatchMouseMovement, 118effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch weak_factory_.GetWeakPtr())); 119f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) } 1205c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return ui::POST_DISPATCH_NONE; 12190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) } 12290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) case ButtonRelease: { 123eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch if (xev->xbutton.button == Button1) { 124eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch // Assume that drags are being done with the left mouse button. Only 125eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch // break the drag if the left mouse button was released. 126effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch DispatchMouseMovement(); 127eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch delegate_->OnMouseReleased(); 128eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } 1295c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return ui::POST_DISPATCH_NONE; 13090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) } 131a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) case KeyPress: { 132c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch if (ui::KeyboardCodeFromXKeyEvent(xev) == ui::VKEY_ESCAPE) { 133c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch canceled_ = true; 134a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) EndMoveLoop(); 1355c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return ui::POST_DISPATCH_NONE; 136c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch } 137a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) break; 138a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) } 1395c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu case GenericEvent: { 1405c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu ui::EventType type = ui::EventTypeFromNative(xev); 1415c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu switch (type) { 1425c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu case ui::ET_MOUSE_MOVED: 1435c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu case ui::ET_MOUSE_DRAGGED: 1445c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu case ui::ET_MOUSE_RELEASED: { 1455c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu XEvent xevent = {0}; 1465c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu if (type == ui::ET_MOUSE_RELEASED) { 1475c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu xevent.type = ButtonRelease; 1485c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu xevent.xbutton.button = ui::EventButtonFromNative(xev); 1495c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } else { 1505c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu xevent.type = MotionNotify; 1515c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 1525c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu xevent.xany.display = xev->xgeneric.display; 1535c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu xevent.xany.window = grab_input_window_; 1545c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu // The fields used below are in the same place for all of events 1555c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu // above. Using xmotion from XEvent's unions to avoid repeating 1565c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu // the code. 1575c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu xevent.xmotion.root = DefaultRootWindow(xev->xgeneric.display); 1585c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu xevent.xmotion.time = ui::EventTimeFromNative(xev).InMilliseconds(); 1595c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu gfx::Point point(ui::EventSystemLocationFromNative(xev)); 1605c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu xevent.xmotion.x_root = point.x(); 1615c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu xevent.xmotion.y_root = point.y(); 1625c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu DispatchEvent(&xevent); 1635c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return ui::POST_DISPATCH_NONE; 1645c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 1655c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu default: 1665c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu break; 1675c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 1685c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu } 16990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) } 17090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 1715c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu return (event->xany.window == grab_input_window_) ? 1725c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu ui::POST_DISPATCH_NONE : ui::POST_DISPATCH_PERFORM_DEFAULT; 17390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)} 17490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 1757dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdochbool X11WholeScreenMoveLoop::RunMoveLoop(aura::Window* source, 1767dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch gfx::NativeCursor cursor) { 1770529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch DCHECK(!in_move_loop_); // Can only handle one nested loop at a time. 1780529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch 179f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) // Start a capture on the host, so that it continues to receive events during 180a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // the drag. This may be second time we are capturing the mouse events - the 181a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // first being when a mouse is first pressed. That first capture needs to be 182a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // released before the call to GrabPointerAndKeyboard below, otherwise it may 183a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // get released while we still need the pointer grab, which is why we restrict 184a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // the scope here. 185a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) { 186a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) ScopedCapturer capturer(source->GetHost()); 187a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 1880529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch grab_input_window_ = CreateDragInputWindow(gfx::GetXDisplay()); 189a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // Releasing ScopedCapturer ensures that any other instance of 190a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // X11ScopedCapture will not prematurely release grab that will be acquired 191a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // below. 192a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) } 193a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // TODO(varkha): Consider integrating GrabPointerAndKeyboard with 194a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // ScopedCapturer to avoid possibility of logically keeping multiple grabs. 1950529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch if (!GrabPointerAndKeyboard(cursor)) { 1960529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch XDestroyWindow(gfx::GetXDisplay(), grab_input_window_); 19790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) return false; 1980529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch } 1990529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch 2005c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu scoped_ptr<ui::ScopedEventDispatcher> old_dispatcher = 2015c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu nested_dispatcher_.Pass(); 2025c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu nested_dispatcher_ = 2035c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu ui::PlatformEventSource::GetInstance()->OverrideDispatcher(this); 2040529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch if (!drag_image_.isNull() && CheckIfIconValid()) 2050529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch CreateDragImageWindow(); 20690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 2071e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles) // We are handling a mouse drag outside of the aura::RootWindow system. We 2081e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles) // must manually make aura think that the mouse button is pressed so that we 2091e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles) // don't draw extraneous tooltips. 2105d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) aura::Env* env = aura::Env::GetInstance(); 2115d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if (!env->IsMouseButtonDown()) { 2125d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) env->set_mouse_button_flags(ui::EF_LEFT_MOUSE_BUTTON); 2135d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) should_reset_mouse_flags_ = true; 2145d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 2151e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles) 2160529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch in_move_loop_ = true; 217c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch canceled_ = false; 21890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) base::MessageLoopForUI* loop = base::MessageLoopForUI::current(); 21990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) base::MessageLoop::ScopedNestableTaskAllower allow_nested(loop); 2205d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) base::RunLoop run_loop; 22190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) quit_closure_ = run_loop.QuitClosure(); 22290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) run_loop.Run(); 2235c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu nested_dispatcher_ = old_dispatcher.Pass(); 224c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch return !canceled_; 22590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)} 22690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 2277dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdochvoid X11WholeScreenMoveLoop::UpdateCursor(gfx::NativeCursor cursor) { 2285d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if (in_move_loop_) { 2295d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) // If we're still in the move loop, regrab the pointer with the updated 2305d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) // cursor. Note: we can be called from handling an XdndStatus message after 2315d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) // EndMoveLoop() was called, but before we return from the nested RunLoop. 232a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) GrabPointerAndKeyboard(cursor); 2335d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 2347dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch} 2357dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch 23690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)void X11WholeScreenMoveLoop::EndMoveLoop() { 23790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) if (!in_move_loop_) 23890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) return; 23990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 240effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch // Prevent DispatchMouseMovement from dispatching any posted motion event. 241effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch weak_factory_.InvalidateWeakPtrs(); 242effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch last_xmotion_.type = LASTEvent; 243effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch 2441e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles) // We undo our emulated mouse click from RunMoveLoop(); 2455d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if (should_reset_mouse_flags_) { 2465d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) aura::Env::GetInstance()->set_mouse_button_flags(0); 2475d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) should_reset_mouse_flags_ = false; 2485d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 2491e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles) 25090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) // TODO(erg): Is this ungrab the cause of having to click to give input focus 25190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) // on drawn out windows? Not ungrabbing here screws the X server until I kill 25290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) // the chrome process. 25390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 25490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) // Ungrab before we let go of the window. 25568043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles) XDisplay* display = gfx::GetXDisplay(); 25690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) XUngrabPointer(display, CurrentTime); 257a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) XUngrabKeyboard(display, CurrentTime); 25890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 2595c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu // Restore the previous dispatcher. 2605c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu nested_dispatcher_.reset(); 261f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) drag_widget_.reset(); 26290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) delegate_->OnMoveLoopEnded(); 26390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) XDestroyWindow(display, grab_input_window_); 2645c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu grab_input_window_ = None; 26590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 26690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) in_move_loop_ = false; 26790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) quit_closure_.Run(); 26890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)} 26990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 270f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)void X11WholeScreenMoveLoop::SetDragImage(const gfx::ImageSkia& image, 271f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) gfx::Vector2dF offset) { 272f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) drag_image_ = image; 273f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) drag_offset_ = offset; 274f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) // Reset the Y offset, so that the drag-image is always just below the cursor, 275f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) // so that it is possible to see where the cursor is going. 276f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) drag_offset_.set_y(0.f); 277f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)} 278f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) 279a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)bool X11WholeScreenMoveLoop::GrabPointerAndKeyboard(gfx::NativeCursor cursor) { 28068043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles) XDisplay* display = gfx::GetXDisplay(); 2817dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch XGrabServer(display); 282a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 2837dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch XUngrabPointer(display, CurrentTime); 2847dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch int ret = XGrabPointer( 2857dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch display, 2867dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch grab_input_window_, 2877dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch False, 2887dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch ButtonPressMask | ButtonReleaseMask | PointerMotionMask, 2897dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch GrabModeAsync, 2907dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch GrabModeAsync, 2917dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch None, 2927dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch cursor.platform(), 2937dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch CurrentTime); 2947dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch if (ret != GrabSuccess) { 295a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) DLOG(ERROR) << "Grabbing pointer for dragging failed: " 2967dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch << ui::GetX11ErrorString(display, ret); 297a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) } else { 298a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) XUngrabKeyboard(display, CurrentTime); 299a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) ret = XGrabKeyboard( 300a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) display, 301a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) grab_input_window_, 302a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) False, 303a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) GrabModeAsync, 304a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) GrabModeAsync, 305a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) CurrentTime); 306a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) if (ret != GrabSuccess) { 307a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) DLOG(ERROR) << "Grabbing keyboard for dragging failed: " 308a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) << ui::GetX11ErrorString(display, ret); 309a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) } 3107dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch } 3117dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch 312a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) XUngrabServer(display); 3135c02ac1a9c1b504631c0a3d2b6e737b5d738bae1Bo Liu XFlush(display); 314a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) return ret == GrabSuccess; 3157dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch} 3167dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch 317f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)Window X11WholeScreenMoveLoop::CreateDragInputWindow(XDisplay* display) { 318f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) // Creates an invisible, InputOnly toplevel window. This window will receive 319f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) // all mouse movement for drags. It turns out that normal windows doing a 320f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) // grab doesn't redirect pointer motion events if the pointer isn't over the 321f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) // grabbing window. But InputOnly windows are able to grab everything. This 322f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) // is what GTK+ does, and I found a patch to KDE that did something similar. 323f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) unsigned long attribute_mask = CWEventMask | CWOverrideRedirect; 324f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) XSetWindowAttributes swa; 325f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) memset(&swa, 0, sizeof(swa)); 326f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) swa.event_mask = ButtonPressMask | ButtonReleaseMask | PointerMotionMask | 327a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) KeyPressMask | KeyReleaseMask | StructureNotifyMask; 328f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) swa.override_redirect = True; 329f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) Window window = XCreateWindow(display, 330f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) DefaultRootWindow(display), 331f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) -100, -100, 10, 10, 332f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) 0, CopyFromParent, InputOnly, CopyFromParent, 333f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) attribute_mask, &swa); 334f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) XMapRaised(display, window); 335c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch ui::X11EventSource::GetInstance()->BlockUntilWindowMapped(window); 336f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) return window; 337f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)} 338f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) 339f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)void X11WholeScreenMoveLoop::CreateDragImageWindow() { 340f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) Widget* widget = new Widget; 341f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) Widget::InitParams params(Widget::InitParams::TYPE_DRAG); 342f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) params.opacity = Widget::InitParams::OPAQUE_WINDOW; 343f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; 344f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) params.accept_events = false; 345f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) 346f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) gfx::Point location = gfx::ToFlooredPoint( 347f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) gfx::Screen::GetNativeScreen()->GetCursorScreenPoint() - drag_offset_); 348f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) params.bounds = gfx::Rect(location, drag_image_.size()); 349f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) widget->set_focus_on_creation(false); 350f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) widget->set_frame_type(Widget::FRAME_TYPE_FORCE_NATIVE); 351f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) widget->Init(params); 352effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch widget->SetOpacity(kDragWidgetOpacity); 353f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) widget->GetNativeWindow()->SetName("DragWindow"); 354f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) 355f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) ImageView* image = new ImageView(); 356f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) image->SetImage(drag_image_); 357f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) image->SetBounds(0, 0, drag_image_.width(), drag_image_.height()); 358f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) widget->SetContentsView(image); 359f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) widget->Show(); 360f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) widget->GetNativeWindow()->layer()->SetFillsBoundsOpaquely(false); 361f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) 362f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) drag_widget_.reset(widget); 363f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)} 364f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) 365a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)bool X11WholeScreenMoveLoop::CheckIfIconValid() { 366a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // TODO(erg): I've tried at least five different strategies for trying to 367a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // build a mask based off the alpha channel. While all of them have worked, 368a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // none of them have been performant and introduced multiple second 369a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // delays. (I spent a day getting a rectangle segmentation algorithm polished 370a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // here...and then found that even through I had the rectangle extraction 371a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // down to mere milliseconds, SkRegion still fell over on the number of 372a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // rectangles.) 373a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // 374a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // Creating a mask here near instantaneously should be possible, as GTK does 375a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // it, but I've blown days on this and I'm punting now. 376a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 377a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) const SkBitmap* in_bitmap = drag_image_.bitmap(); 378a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) SkAutoLockPixels in_lock(*in_bitmap); 379a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) for (int y = 0; y < in_bitmap->height(); ++y) { 380a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) uint32* in_row = in_bitmap->getAddr32(0, y); 381a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 382a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) for (int x = 0; x < in_bitmap->width(); ++x) { 383a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) if (SkColorGetA(in_row[x]) > kMinAlpha) 384a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) return true; 385a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) } 386a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) } 387a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 388a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) return false; 389a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)} 390a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 39190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)} // namespace views 392