// Copyright 2016 The Chromium Embedded Framework 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/views/window_view.h" #include "libcef/browser/image_impl.h" #include "libcef/browser/views/window_impl.h" #include "third_party/skia/include/core/SkRegion.h" #include "ui/aura/window.h" #include "ui/base/hit_test.h" #include "ui/views/widget/widget.h" #include "ui/views/window/native_frame_view.h" #if defined(OS_LINUX) && defined(USE_X11) #include #include "ui/gfx/x/x11_types.h" #endif #if defined(OS_WIN) #include "ui/display/screen.h" #include "ui/views/win/hwnd_util.h" #endif namespace { // Specialize ClientView to handle Widget-related events. class ClientViewEx : public views::ClientView { public: ClientViewEx(views::Widget* widget, views::View* contents_view, CefWindowView::Delegate* window_delegate) : views::ClientView(widget, contents_view), window_delegate_(window_delegate) { DCHECK(window_delegate_); } bool CanClose() override { return window_delegate_->CanWidgetClose(); } private: CefWindowView::Delegate* window_delegate_; // Not owned by this object. DISALLOW_COPY_AND_ASSIGN(ClientViewEx); }; // Extend NativeFrameView with draggable region handling. class NativeFrameViewEx : public views::NativeFrameView { public: NativeFrameViewEx(views::Widget* widget, CefWindowView* view) : views::NativeFrameView(widget), widget_(widget), view_(view) {} gfx::Rect GetWindowBoundsForClientBounds( const gfx::Rect& client_bounds) const override { #if defined(OS_WIN) // views::GetWindowBoundsForClientBounds() expects the input Rect to be in // pixel coordinates. NativeFrameView does not implement this correctly so // we need to provide our own implementation. See http://crbug.com/602692. gfx::Rect pixel_bounds = display::Screen::GetScreen()->DIPToScreenRectInWindow( view_util::GetNativeWindow(widget_), client_bounds); pixel_bounds = views::GetWindowBoundsForClientBounds( static_cast(const_cast(this)), pixel_bounds); return display::Screen::GetScreen()->ScreenToDIPRectInWindow( view_util::GetNativeWindow(widget_), pixel_bounds); #else // Use the default implementation. return views::NativeFrameView::GetWindowBoundsForClientBounds( client_bounds); #endif } int NonClientHitTest(const gfx::Point& point) override { if (widget_->IsFullscreen()) return HTCLIENT; // Test for mouse clicks that fall within the draggable region. SkRegion* draggable_region = view_->draggable_region(); if (draggable_region && draggable_region->contains(point.x(), point.y())) return HTCAPTION; return views::NativeFrameView::NonClientHitTest(point); } private: // Not owned by this object. views::Widget* widget_; CefWindowView* view_; DISALLOW_COPY_AND_ASSIGN(NativeFrameViewEx); }; // The area inside the frame border that can be clicked and dragged for resizing // the window. Only used in restored mode. const int kResizeBorderThickness = 4; // The distance from each window corner that triggers diagonal resizing. Only // used in restored mode. const int kResizeAreaCornerSize = 16; // Implement NonClientFrameView without the system default caption and icon but // with a resizable border. Based on AppWindowFrameView and CustomFrameView. class CaptionlessFrameView : public views::NonClientFrameView { public: CaptionlessFrameView(views::Widget* widget, CefWindowView* view) : widget_(widget), view_(view) {} gfx::Rect GetBoundsForClientView() const override { return client_view_bounds_; } gfx::Rect GetWindowBoundsForClientBounds( const gfx::Rect& client_bounds) const override { return client_bounds; } int NonClientHitTest(const gfx::Point& point) override { if (widget_->IsFullscreen()) return HTCLIENT; // Sanity check. if (!bounds().Contains(point)) return HTNOWHERE; // Check the frame first, as we allow a small area overlapping the contents // to be used for resize handles. bool can_ever_resize = widget_->widget_delegate() ? widget_->widget_delegate()->CanResize() : false; // Don't allow overlapping resize handles when the window is maximized or // fullscreen, as it can't be resized in those states. int resize_border_thickness = ResizeBorderThickness(); int frame_component = GetHTComponentForFrame( point, resize_border_thickness, resize_border_thickness, kResizeAreaCornerSize, kResizeAreaCornerSize, can_ever_resize); if (frame_component != HTNOWHERE) return frame_component; // Test for mouse clicks that fall within the draggable region. SkRegion* draggable_region = view_->draggable_region(); if (draggable_region && draggable_region->contains(point.x(), point.y())) return HTCAPTION; int client_component = widget_->client_view()->NonClientHitTest(point); if (client_component != HTNOWHERE) return client_component; // Caption is a safe default. return HTCAPTION; } void GetWindowMask(const gfx::Size& size, SkPath* window_mask) override { // Nothing to do here. } void ResetWindowControls() override { // Nothing to do here. } void UpdateWindowIcon() override { // Nothing to do here. } void UpdateWindowTitle() override { // Nothing to do here. } void SizeConstraintsChanged() override { // Nothing to do here. } void OnPaint(gfx::Canvas* canvas) override { // Nothing to do here. } void Layout() override { client_view_bounds_.SetRect(0, 0, width(), height()); } gfx::Size CalculatePreferredSize() const override { return widget_->non_client_view() ->GetWindowBoundsForClientBounds( gfx::Rect(widget_->client_view()->GetPreferredSize())) .size(); } gfx::Size GetMinimumSize() const override { return widget_->non_client_view() ->GetWindowBoundsForClientBounds( gfx::Rect(widget_->client_view()->GetMinimumSize())) .size(); } gfx::Size GetMaximumSize() const override { gfx::Size max_size = widget_->client_view()->GetMaximumSize(); gfx::Size converted_size = widget_->non_client_view() ->GetWindowBoundsForClientBounds(gfx::Rect(max_size)) .size(); return gfx::Size(max_size.width() == 0 ? 0 : converted_size.width(), max_size.height() == 0 ? 0 : converted_size.height()); } private: int ResizeBorderThickness() const { return (widget_->IsMaximized() || widget_->IsFullscreen() ? 0 : kResizeBorderThickness); } // Not owned by this object. views::Widget* widget_; CefWindowView* view_; // The bounds of the client view, in this view's coordinates. gfx::Rect client_view_bounds_; DISALLOW_COPY_AND_ASSIGN(CaptionlessFrameView); }; bool IsWindowBorderHit(int code) { // On Windows HTLEFT = 10 and HTBORDER = 18. Values are not ordered the same // in base/hit_test.h for non-Windows platforms. #if defined(OS_WIN) return code >= HTLEFT && code <= HTBORDER; #else return code == HTLEFT || code == HTRIGHT || code == HTTOP || code == HTTOPLEFT || code == HTTOPRIGHT || code == HTBOTTOM || code == HTBOTTOMLEFT || code == HTBOTTOMRIGHT || code == HTBORDER; #endif } } // namespace CefWindowView::CefWindowView(CefWindowDelegate* cef_delegate, Delegate* window_delegate) : ParentClass(cef_delegate), window_delegate_(window_delegate), is_frameless_(false) { DCHECK(window_delegate_); } void CefWindowView::CreateWidget() { DCHECK(!GetWidget()); // |widget| is owned by the NativeWidget and will be destroyed in response to // a native destruction message. views::Widget* widget = new views::Widget; views::Widget::InitParams params; params.delegate = this; params.type = views::Widget::InitParams::TYPE_WINDOW; params.bounds = gfx::Rect(CalculatePreferredSize()); bool can_activate = true; if (cef_delegate()) { CefRefPtr cef_window = GetCefWindow(); is_frameless_ = cef_delegate()->IsFrameless(cef_window); bool is_menu = false; bool can_activate_menu = true; CefRefPtr parent_window = cef_delegate()->GetParentWindow( cef_window, &is_menu, &can_activate_menu); if (parent_window && !parent_window->IsSame(cef_window)) { CefWindowImpl* parent_window_impl = static_cast(parent_window.get()); params.parent = view_util::GetNativeWindow(parent_window_impl->widget()); if (is_menu) { // Don't clip the window to parent bounds. params.type = views::Widget::InitParams::TYPE_MENU; // Don't set "always on top" for the window. params.z_order = ui::ZOrderLevel::kNormal; can_activate = can_activate_menu; if (can_activate_menu) params.activatable = views::Widget::InitParams::ACTIVATABLE_YES; } } } #if defined(OS_WIN) if (is_frameless_) { // Don't show the native window caption. Setting this value on Linux will // result in window resize artifacts. params.remove_standard_frame = true; } #endif widget->Init(std::move(params)); // |widget| should now be associated with |this|. DCHECK_EQ(widget, GetWidget()); // |widget| must be top-level for focus handling to work correctly. DCHECK(widget->is_top_level()); if (can_activate) { // |widget| must be activatable for focus handling to work correctly. DCHECK(widget->widget_delegate()->CanActivate()); } #if defined(OS_LINUX) && defined(USE_X11) if (is_frameless_) { ::Window window = view_util::GetWindowHandle(widget); DCHECK(window); ::Display* display = gfx::GetXDisplay(); DCHECK(display); // Make the window borderless. From // http://stackoverflow.com/questions/1904445/borderless-windows-on-linux struct MwmHints { unsigned long flags; unsigned long functions; unsigned long decorations; long input_mode; unsigned long status; }; enum { MWM_HINTS_FUNCTIONS = (1L << 0), MWM_HINTS_DECORATIONS = (1L << 1), MWM_FUNC_ALL = (1L << 0), MWM_FUNC_RESIZE = (1L << 1), MWM_FUNC_MOVE = (1L << 2), MWM_FUNC_MINIMIZE = (1L << 3), MWM_FUNC_MAXIMIZE = (1L << 4), MWM_FUNC_CLOSE = (1L << 5) }; Atom mwmHintsProperty = XInternAtom(display, "_MOTIF_WM_HINTS", 0); struct MwmHints hints = {}; hints.flags = MWM_HINTS_DECORATIONS; hints.decorations = 0; XChangeProperty(display, window, mwmHintsProperty, mwmHintsProperty, 32, PropModeReplace, (unsigned char*)&hints, 5); } #endif // defined(OS_LINUX) && defined(USE_X11) } CefRefPtr CefWindowView::GetCefWindow() const { CefRefPtr window = GetCefPanel()->AsWindow(); DCHECK(window); return window; } void CefWindowView::DeleteDelegate() { // Remove all child Views before deleting the Window so that notifications // resolve correctly. RemoveAllChildViews(true); window_delegate_->OnWindowViewDeleted(); // Deletes |this|. views::WidgetDelegateView::DeleteDelegate(); } bool CefWindowView::CanResize() const { if (!cef_delegate()) return true; return cef_delegate()->CanResize(GetCefWindow()); } bool CefWindowView::CanMinimize() const { if (!cef_delegate()) return true; return cef_delegate()->CanMinimize(GetCefWindow()); } bool CefWindowView::CanMaximize() const { if (!cef_delegate()) return true; return cef_delegate()->CanMaximize(GetCefWindow()); } base::string16 CefWindowView::GetWindowTitle() const { return title_; } gfx::ImageSkia CefWindowView::GetWindowIcon() { if (!window_icon_) return ParentClass::GetWindowIcon(); return static_cast(window_icon_.get()) ->GetForced1xScaleRepresentation(GetDisplay().device_scale_factor()); } gfx::ImageSkia CefWindowView::GetWindowAppIcon() { if (!window_app_icon_) return ParentClass::GetWindowAppIcon(); return static_cast(window_app_icon_.get()) ->GetForced1xScaleRepresentation(GetDisplay().device_scale_factor()); } void CefWindowView::WindowClosing() { window_delegate_->OnWindowClosing(); } views::View* CefWindowView::GetContentsView() { // |this| will be the "Contents View" hosted by the Widget via ClientView and // RootView. return this; } views::ClientView* CefWindowView::CreateClientView(views::Widget* widget) { return new ClientViewEx(widget, GetContentsView(), window_delegate_); } views::NonClientFrameView* CefWindowView::CreateNonClientFrameView( views::Widget* widget) { if (is_frameless_) { // Custom frame type that doesn't render a caption. return new CaptionlessFrameView(widget, this); } else if (widget->ShouldUseNativeFrame()) { // DesktopNativeWidgetAura::CreateNonClientFrameView() returns // NativeFrameView by default. Extend that type. return new NativeFrameViewEx(widget, this); } // Use Chromium provided CustomFrameView. In case if we would like to // customize the frame, provide own implementation. return nullptr; } bool CefWindowView::ShouldDescendIntoChildForEventHandling( gfx::NativeView child, const gfx::Point& location) { if (is_frameless_) { // If the window is resizable it should claim mouse events that fall on the // window border. views::NonClientFrameView* ncfv = GetNonClientFrameView(); if (ncfv) { int result = ncfv->NonClientHitTest(location); if (IsWindowBorderHit(result)) return false; } } // The window should claim mouse events that fall within the draggable region. return !draggable_region_.get() || !draggable_region_->contains(location.x(), location.y()); } bool CefWindowView::MaybeGetMinimumSize(gfx::Size* size) const { #if defined(OS_LINUX) // Resize is disabled on Linux by returning the preferred size as the min/max // size. if (!CanResize()) { *size = CalculatePreferredSize(); return true; } #endif return false; } bool CefWindowView::MaybeGetMaximumSize(gfx::Size* size) const { #if defined(OS_LINUX) // Resize is disabled on Linux by returning the preferred size as the min/max // size. if (!CanResize()) { *size = CalculatePreferredSize(); return true; } #endif return false; } void CefWindowView::ViewHierarchyChanged( const views::ViewHierarchyChangedDetails& details) { if (details.child == this) { // This View's parent types (RootView, ClientView) are not exposed via the // CEF API. Therefore don't send notifications about this View's parent // changes. return; } ParentClass::ViewHierarchyChanged(details); } display::Display CefWindowView::GetDisplay() const { const views::Widget* widget = GetWidget(); if (widget) { return view_util::GetDisplayMatchingBounds( widget->GetWindowBoundsInScreen(), false); } return display::Display(); } void CefWindowView::SetTitle(const base::string16& title) { title_ = title; views::Widget* widget = GetWidget(); if (widget) widget->UpdateWindowTitle(); } void CefWindowView::SetWindowIcon(CefRefPtr window_icon) { if (std::max(window_icon->GetWidth(), window_icon->GetHeight()) != 16U) { DLOG(ERROR) << "Window icons must be 16 DIP in size."; return; } window_icon_ = window_icon; views::Widget* widget = GetWidget(); if (widget) widget->UpdateWindowIcon(); } void CefWindowView::SetWindowAppIcon(CefRefPtr window_app_icon) { window_app_icon_ = window_app_icon; views::Widget* widget = GetWidget(); if (widget) widget->UpdateWindowIcon(); } void CefWindowView::SetDraggableRegions( const std::vector& regions) { if (regions.empty()) { if (draggable_region_) draggable_region_.reset(nullptr); return; } draggable_region_.reset(new SkRegion); for (const CefDraggableRegion& region : regions) { draggable_region_->op( {region.bounds.x, region.bounds.y, region.bounds.x + region.bounds.width, region.bounds.y + region.bounds.height}, region.draggable ? SkRegion::kUnion_Op : SkRegion::kDifference_Op); } } views::NonClientFrameView* CefWindowView::GetNonClientFrameView() const { const views::Widget* widget = GetWidget(); if (!widget) return nullptr; if (!widget->non_client_view()) return nullptr; return widget->non_client_view()->frame_view(); }