| // Copyright (c) 2012 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 "ash/wm/frame_painter.h" |
| |
| #include <vector> |
| |
| #include "ash/ash_constants.h" |
| #include "ash/root_window_controller.h" |
| #include "ash/shell.h" |
| #include "ash/shell_window_ids.h" |
| #include "ash/wm/property_util.h" |
| #include "ash/wm/window_properties.h" |
| #include "ash/wm/window_util.h" |
| #include "base/logging.h" // DCHECK |
| #include "grit/ash_resources.h" |
| #include "third_party/skia/include/core/SkCanvas.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "third_party/skia/include/core/SkPaint.h" |
| #include "third_party/skia/include/core/SkPath.h" |
| #include "ui/aura/client/aura_constants.h" |
| #include "ui/aura/env.h" |
| #include "ui/aura/root_window.h" |
| #include "ui/aura/window.h" |
| #include "ui/base/animation/slide_animation.h" |
| #include "ui/base/hit_test.h" |
| #include "ui/base/layout.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/base/theme_provider.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/font.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/gfx/screen.h" |
| #include "ui/gfx/skia_util.h" |
| #include "ui/views/controls/button/image_button.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/widget/widget_delegate.h" |
| |
| using aura::RootWindow; |
| using aura::Window; |
| using views::Widget; |
| |
| namespace { |
| // TODO(jamescook): Border is specified to be a single pixel overlapping |
| // the web content and may need to be built into the shadow layers instead. |
| const int kBorderThickness = 0; |
| // Space between left edge of window and popup window icon. |
| const int kIconOffsetX = 9; |
| // Height and width of window icon. |
| const int kIconSize = 16; |
| // Space between the title text and the caption buttons. |
| const int kTitleLogoSpacing = 5; |
| // Space between window icon and title text. |
| const int kTitleIconOffsetX = 5; |
| // Space between window edge and title text, when there is no icon. |
| const int kTitleNoIconOffsetX = 8; |
| // Color for the non-maximized window title text. |
| const SkColor kNonMaximizedWindowTitleTextColor = SkColorSetRGB(40, 40, 40); |
| // Color for the maximized window title text. |
| const SkColor kMaximizedWindowTitleTextColor = SK_ColorWHITE; |
| // Size of header/content separator line below the header image. |
| const int kHeaderContentSeparatorSize = 1; |
| // Color of header bottom edge line. |
| const SkColor kHeaderContentSeparatorColor = SkColorSetRGB(128, 128, 128); |
| // Space between close button and right edge of window. |
| const int kCloseButtonOffsetX = 0; |
| // Space between close button and top edge of window. |
| const int kCloseButtonOffsetY = 0; |
| // The size and close buttons are designed to slightly overlap in order |
| // to do fancy hover highlighting. |
| const int kSizeButtonOffsetX = -1; |
| // In the pre-Ash era the web content area had a frame along the left edge, so |
| // user-generated theme images for the new tab page assume they are shifted |
| // right relative to the header. Now that we have removed the left edge frame |
| // we need to copy the theme image for the window header from a few pixels |
| // inset to preserve alignment with the NTP image, or else we'll break a bunch |
| // of existing themes. We do something similar on OS X for the same reason. |
| const int kThemeFrameImageInsetX = 5; |
| // Duration of crossfade animation for activating and deactivating frame. |
| const int kActivationCrossfadeDurationMs = 200; |
| // Alpha/opacity value for fully-opaque headers. |
| const int kFullyOpaque = 255; |
| |
| // Tiles an image into an area, rounding the top corners. Samples |image| |
| // starting |image_inset_x| pixels from the left of the image. |
| void TileRoundRect(gfx::Canvas* canvas, |
| const gfx::ImageSkia& image, |
| const SkPaint& paint, |
| const gfx::Rect& bounds, |
| int top_left_corner_radius, |
| int top_right_corner_radius, |
| int image_inset_x) { |
| SkRect rect = gfx::RectToSkRect(bounds); |
| const SkScalar kTopLeftRadius = SkIntToScalar(top_left_corner_radius); |
| const SkScalar kTopRightRadius = SkIntToScalar(top_right_corner_radius); |
| SkScalar radii[8] = { |
| kTopLeftRadius, kTopLeftRadius, // top-left |
| kTopRightRadius, kTopRightRadius, // top-right |
| 0, 0, // bottom-right |
| 0, 0}; // bottom-left |
| SkPath path; |
| path.addRoundRect(rect, radii, SkPath::kCW_Direction); |
| canvas->DrawImageInPath(image, -image_inset_x, 0, path, paint); |
| } |
| |
| // Tiles |frame_image| and |frame_overlay_image| into an area, rounding the top |
| // corners. |
| void PaintFrameImagesInRoundRect(gfx::Canvas* canvas, |
| const gfx::ImageSkia* frame_image, |
| const gfx::ImageSkia* frame_overlay_image, |
| const SkPaint& paint, |
| const gfx::Rect& bounds, |
| int corner_radius, |
| int image_inset_x) { |
| SkXfermode::Mode normal_mode; |
| SkXfermode::AsMode(NULL, &normal_mode); |
| |
| // If |paint| is using an unusual SkXfermode::Mode (this is the case while |
| // crossfading), we must create a new canvas to overlay |frame_image| and |
| // |frame_overlay_image| using |normal_mode| and then paint the result |
| // using the unusual mode. We try to avoid this because creating a new |
| // browser-width canvas is expensive. |
| bool fast_path = (!frame_overlay_image || |
| SkXfermode::IsMode(paint.getXfermode(), normal_mode)); |
| if (fast_path) { |
| TileRoundRect(canvas, *frame_image, paint, bounds, corner_radius, |
| corner_radius, image_inset_x); |
| |
| if (frame_overlay_image) { |
| // Adjust |bounds| such that |frame_overlay_image| is not tiled. |
| gfx::Rect overlay_bounds = bounds; |
| overlay_bounds.Intersect( |
| gfx::Rect(bounds.origin(), frame_overlay_image->size())); |
| int top_left_corner_radius = corner_radius; |
| int top_right_corner_radius = corner_radius; |
| if (overlay_bounds.width() < bounds.width() - corner_radius) |
| top_right_corner_radius = 0; |
| TileRoundRect(canvas, *frame_overlay_image, paint, overlay_bounds, |
| top_left_corner_radius, top_right_corner_radius, 0); |
| } |
| } else { |
| gfx::Canvas temporary_canvas(bounds.size(), canvas->scale_factor(), false); |
| temporary_canvas.TileImageInt(*frame_image, |
| image_inset_x, 0, |
| 0, 0, |
| bounds.width(), bounds.height()); |
| temporary_canvas.DrawImageInt(*frame_overlay_image, 0, 0); |
| TileRoundRect(canvas, gfx::ImageSkia(temporary_canvas.ExtractImageRep()), |
| paint, bounds, corner_radius, corner_radius, 0); |
| } |
| } |
| |
| // Returns true if |child| and all ancestors are visible. Useful to ensure that |
| // a window is individually visible and is not part of a hidden workspace. |
| bool IsVisibleToRoot(Window* child) { |
| for (Window* window = child; window; window = window->parent()) { |
| // We must use TargetVisibility() because windows animate in and out and |
| // IsVisible() also tracks the layer visibility state. |
| if (!window->TargetVisibility()) |
| return false; |
| } |
| return true; |
| } |
| |
| // Returns true if |window| is a "normal" window for purposes of solo window |
| // computations. Returns false for windows that are: |
| // * Not drawn (for example, DragDropTracker uses one for mouse capture) |
| // * Modal alerts (it looks odd for headers to change when an alert opens) |
| // * Constrained windows (ditto) |
| bool IsSoloWindowHeaderCandidate(aura::Window* window) { |
| return window && |
| window->type() == aura::client::WINDOW_TYPE_NORMAL && |
| window->layer() && |
| window->layer()->type() != ui::LAYER_NOT_DRAWN && |
| window->GetProperty(aura::client::kModalKey) == ui::MODAL_TYPE_NONE && |
| !window->GetProperty(ash::kConstrainedWindowKey); |
| } |
| |
| // Returns a list of windows in |root_window|| that potentially could have |
| // a transparent solo-window header. |
| std::vector<Window*> GetWindowsForSoloHeaderUpdate(RootWindow* root_window) { |
| std::vector<Window*> windows; |
| // During shutdown there may not be a workspace controller. In that case |
| // we don't care about updating any windows. |
| // Avoid memory allocations for typical window counts. |
| windows.reserve(16); |
| // Collect windows from the desktop. |
| Window* desktop = ash::Shell::GetContainer( |
| root_window, ash::internal::kShellWindowId_DefaultContainer); |
| windows.insert(windows.end(), |
| desktop->children().begin(), |
| desktop->children().end()); |
| // Collect "always on top" windows. |
| Window* top_container = |
| ash::Shell::GetContainer( |
| root_window, ash::internal::kShellWindowId_AlwaysOnTopContainer); |
| windows.insert(windows.end(), |
| top_container->children().begin(), |
| top_container->children().end()); |
| return windows; |
| } |
| } // namespace |
| |
| namespace ash { |
| |
| // static |
| int FramePainter::kActiveWindowOpacity = 255; // 1.0 |
| int FramePainter::kInactiveWindowOpacity = 255; // 1.0 |
| int FramePainter::kSoloWindowOpacity = 77; // 0.3 |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // FramePainter, public: |
| |
| FramePainter::FramePainter() |
| : frame_(NULL), |
| window_icon_(NULL), |
| size_button_(NULL), |
| close_button_(NULL), |
| window_(NULL), |
| button_separator_(NULL), |
| top_left_corner_(NULL), |
| top_edge_(NULL), |
| top_right_corner_(NULL), |
| header_left_edge_(NULL), |
| header_right_edge_(NULL), |
| previous_theme_frame_id_(0), |
| previous_theme_frame_overlay_id_(0), |
| previous_opacity_(0), |
| crossfade_theme_frame_id_(0), |
| crossfade_theme_frame_overlay_id_(0), |
| crossfade_opacity_(0), |
| size_button_behavior_(SIZE_BUTTON_MAXIMIZES) {} |
| |
| FramePainter::~FramePainter() { |
| // Sometimes we are destroyed before the window closes, so ensure we clean up. |
| if (window_) { |
| window_->RemoveObserver(this); |
| } |
| } |
| |
| void FramePainter::Init(views::Widget* frame, |
| views::View* window_icon, |
| views::ImageButton* size_button, |
| views::ImageButton* close_button, |
| SizeButtonBehavior behavior) { |
| DCHECK(frame); |
| // window_icon may be NULL. |
| DCHECK(size_button); |
| DCHECK(close_button); |
| frame_ = frame; |
| window_icon_ = window_icon; |
| size_button_ = size_button; |
| close_button_ = close_button; |
| size_button_behavior_ = behavior; |
| |
| // Window frame image parts. |
| ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); |
| button_separator_ = |
| rb.GetImageNamed(IDR_AURA_WINDOW_BUTTON_SEPARATOR).ToImageSkia(); |
| top_left_corner_ = |
| rb.GetImageNamed(IDR_AURA_WINDOW_HEADER_SHADE_TOP_LEFT).ToImageSkia(); |
| top_edge_ = |
| rb.GetImageNamed(IDR_AURA_WINDOW_HEADER_SHADE_TOP).ToImageSkia(); |
| top_right_corner_ = |
| rb.GetImageNamed(IDR_AURA_WINDOW_HEADER_SHADE_TOP_RIGHT).ToImageSkia(); |
| header_left_edge_ = |
| rb.GetImageNamed(IDR_AURA_WINDOW_HEADER_SHADE_LEFT).ToImageSkia(); |
| header_right_edge_ = |
| rb.GetImageNamed(IDR_AURA_WINDOW_HEADER_SHADE_RIGHT).ToImageSkia(); |
| |
| window_ = frame->GetNativeWindow(); |
| gfx::Insets mouse_insets = gfx::Insets(-kResizeOutsideBoundsSize, |
| -kResizeOutsideBoundsSize, |
| -kResizeOutsideBoundsSize, |
| -kResizeOutsideBoundsSize); |
| gfx::Insets touch_insets = mouse_insets.Scale( |
| kResizeOutsideBoundsScaleForTouch); |
| // Ensure we get resize cursors for a few pixels outside our bounds. |
| window_->SetHitTestBoundsOverrideOuter(mouse_insets, touch_insets); |
| // Ensure we get resize cursors just inside our bounds as well. |
| window_->set_hit_test_bounds_override_inner(mouse_insets); |
| |
| // Watch for maximize/restore/fullscreen state changes. Observer removes |
| // itself in OnWindowDestroying() below, or in the destructor if we go away |
| // before the window. |
| window_->AddObserver(this); |
| |
| // Solo-window header updates are handled by the workspace controller when |
| // this window is added to the desktop. |
| } |
| |
| // static |
| void FramePainter::UpdateSoloWindowHeader(RootWindow* root_window) { |
| // Use a separate function here so callers outside of FramePainter don't need |
| // to know about "ignorable_window". |
| UpdateSoloWindowInRoot(root_window, NULL /* ignorable_window */); |
| } |
| |
| gfx::Rect FramePainter::GetBoundsForClientView( |
| int top_height, |
| const gfx::Rect& window_bounds) const { |
| return gfx::Rect( |
| kBorderThickness, |
| top_height, |
| std::max(0, window_bounds.width() - (2 * kBorderThickness)), |
| std::max(0, window_bounds.height() - top_height - kBorderThickness)); |
| } |
| |
| gfx::Rect FramePainter::GetWindowBoundsForClientBounds( |
| int top_height, |
| const gfx::Rect& client_bounds) const { |
| return gfx::Rect(std::max(0, client_bounds.x() - kBorderThickness), |
| std::max(0, client_bounds.y() - top_height), |
| client_bounds.width() + (2 * kBorderThickness), |
| client_bounds.height() + top_height + kBorderThickness); |
| } |
| |
| int FramePainter::NonClientHitTest(views::NonClientFrameView* view, |
| const gfx::Point& point) { |
| gfx::Rect expanded_bounds = view->bounds(); |
| int outside_bounds = kResizeOutsideBoundsSize; |
| |
| if (aura::Env::GetInstance()->is_touch_down()) |
| outside_bounds *= kResizeOutsideBoundsScaleForTouch; |
| expanded_bounds.Inset(-outside_bounds, -outside_bounds); |
| |
| if (!expanded_bounds.Contains(point)) |
| return HTNOWHERE; |
| |
| // No avatar button. |
| |
| // Check the frame first, as we allow a small area overlapping the contents |
| // to be used for resize handles. |
| bool can_ever_resize = frame_->widget_delegate() ? |
| frame_->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 = |
| frame_->IsMaximized() || frame_->IsFullscreen() ? 0 : |
| kResizeInsideBoundsSize; |
| int frame_component = view->GetHTComponentForFrame(point, |
| resize_border, |
| resize_border, |
| kResizeAreaCornerSize, |
| kResizeAreaCornerSize, |
| can_ever_resize); |
| if (frame_component != HTNOWHERE) |
| return frame_component; |
| |
| int client_component = frame_->client_view()->NonClientHitTest(point); |
| if (client_component != HTNOWHERE) |
| return client_component; |
| |
| // Then see if the point is within any of the window controls. |
| if (close_button_->visible() && |
| close_button_->GetMirroredBounds().Contains(point)) |
| return HTCLOSE; |
| if (size_button_->visible() && |
| size_button_->GetMirroredBounds().Contains(point)) |
| return HTMAXBUTTON; |
| |
| // Caption is a safe default. |
| return HTCAPTION; |
| } |
| |
| gfx::Size FramePainter::GetMinimumSize(views::NonClientFrameView* view) { |
| gfx::Size min_size = frame_->client_view()->GetMinimumSize(); |
| // Ensure we can display the top of the caption area. |
| gfx::Rect client_bounds = view->GetBoundsForClientView(); |
| min_size.Enlarge(0, client_bounds.y()); |
| // Ensure we have enough space for the window icon and buttons. We allow |
| // the title string to collapse to zero width. |
| int title_width = GetTitleOffsetX() + |
| size_button_->width() + kSizeButtonOffsetX + |
| close_button_->width() + kCloseButtonOffsetX; |
| if (title_width > min_size.width()) |
| min_size.set_width(title_width); |
| return min_size; |
| } |
| |
| gfx::Size FramePainter::GetMaximumSize(views::NonClientFrameView* view) { |
| return frame_->client_view()->GetMaximumSize(); |
| } |
| |
| int FramePainter::GetRightInset() const { |
| gfx::Size close_size = close_button_->GetPreferredSize(); |
| gfx::Size size_button_size = size_button_->GetPreferredSize(); |
| int inset = close_size.width() + kCloseButtonOffsetX + |
| size_button_size.width() + kSizeButtonOffsetX; |
| return inset; |
| } |
| |
| int FramePainter::GetThemeBackgroundXInset() const { |
| return kThemeFrameImageInsetX; |
| } |
| |
| bool FramePainter::ShouldUseMinimalHeaderStyle(Themed header_themed) const { |
| // Use the minimalistic header style whenever |frame_| is maximized or |
| // fullscreen EXCEPT: |
| // - If the user has installed a theme with custom images for the header. |
| // - For windows which are not tracked by the workspace code (which are used |
| // for tab dragging). |
| return ((frame_->IsMaximized() || frame_->IsFullscreen()) && |
| header_themed == THEMED_NO && |
| GetTrackedByWorkspace(frame_->GetNativeWindow())); |
| } |
| |
| void FramePainter::PaintHeader(views::NonClientFrameView* view, |
| gfx::Canvas* canvas, |
| HeaderMode header_mode, |
| int theme_frame_id, |
| int theme_frame_overlay_id) { |
| bool initial_paint = (previous_theme_frame_id_ == 0); |
| if (!initial_paint && |
| (previous_theme_frame_id_ != theme_frame_id || |
| previous_theme_frame_overlay_id_ != theme_frame_overlay_id)) { |
| aura::Window* parent = frame_->GetNativeWindow()->parent(); |
| // Don't animate the header if the parent (a workspace) is already |
| // animating. Doing so results in continually painting during the animation |
| // and gives a slower frame rate. |
| // TODO(sky): expose a better way to determine this rather than assuming |
| // the parent is a workspace. |
| bool parent_animating = parent && |
| (parent->layer()->GetAnimator()->IsAnimatingProperty( |
| ui::LayerAnimationElement::OPACITY) || |
| parent->layer()->GetAnimator()->IsAnimatingProperty( |
| ui::LayerAnimationElement::VISIBILITY)); |
| if (!parent_animating) { |
| crossfade_animation_.reset(new ui::SlideAnimation(this)); |
| crossfade_theme_frame_id_ = previous_theme_frame_id_; |
| crossfade_theme_frame_overlay_id_ = previous_theme_frame_overlay_id_; |
| crossfade_opacity_ = previous_opacity_; |
| crossfade_animation_->SetSlideDuration(kActivationCrossfadeDurationMs); |
| crossfade_animation_->Show(); |
| } else { |
| crossfade_animation_.reset(); |
| } |
| } |
| |
| int opacity = |
| GetHeaderOpacity(header_mode, theme_frame_id, theme_frame_overlay_id); |
| ui::ThemeProvider* theme_provider = frame_->GetThemeProvider(); |
| gfx::ImageSkia* theme_frame = theme_provider->GetImageSkiaNamed( |
| theme_frame_id); |
| gfx::ImageSkia* theme_frame_overlay = NULL; |
| if (theme_frame_overlay_id != 0) { |
| theme_frame_overlay = theme_provider->GetImageSkiaNamed( |
| theme_frame_overlay_id); |
| } |
| header_frame_bounds_ = gfx::Rect(0, 0, view->width(), theme_frame->height()); |
| |
| int corner_radius = GetHeaderCornerRadius(); |
| SkPaint paint; |
| |
| if (crossfade_animation_.get() && crossfade_animation_->is_animating()) { |
| gfx::ImageSkia* crossfade_theme_frame = |
| theme_provider->GetImageSkiaNamed(crossfade_theme_frame_id_); |
| gfx::ImageSkia* crossfade_theme_frame_overlay = NULL; |
| if (crossfade_theme_frame_overlay_id_ != 0) { |
| crossfade_theme_frame_overlay = theme_provider->GetImageSkiaNamed( |
| crossfade_theme_frame_overlay_id_); |
| } |
| if (!crossfade_theme_frame || |
| (crossfade_theme_frame_overlay_id_ != 0 && |
| !crossfade_theme_frame_overlay)) { |
| // Reset the animation. This case occurs when the user switches the theme |
| // that they are using. |
| crossfade_animation_.reset(); |
| paint.setAlpha(opacity); |
| } else { |
| double current_value = crossfade_animation_->GetCurrentValue(); |
| int old_alpha = (1 - current_value) * crossfade_opacity_; |
| int new_alpha = current_value * opacity; |
| |
| // Draw the old header background, clipping the corners to be rounded. |
| paint.setAlpha(old_alpha); |
| paint.setXfermodeMode(SkXfermode::kPlus_Mode); |
| PaintFrameImagesInRoundRect(canvas, |
| crossfade_theme_frame, |
| crossfade_theme_frame_overlay, |
| paint, |
| header_frame_bounds_, |
| corner_radius, |
| GetThemeBackgroundXInset()); |
| |
| paint.setAlpha(new_alpha); |
| } |
| } else { |
| paint.setAlpha(opacity); |
| } |
| |
| // Draw the header background, clipping the corners to be rounded. |
| PaintFrameImagesInRoundRect(canvas, |
| theme_frame, |
| theme_frame_overlay, |
| paint, |
| header_frame_bounds_, |
| corner_radius, |
| GetThemeBackgroundXInset()); |
| |
| previous_theme_frame_id_ = theme_frame_id; |
| previous_theme_frame_overlay_id_ = theme_frame_overlay_id; |
| previous_opacity_ = opacity; |
| |
| // Separator between the maximize and close buttons. It overlaps the left |
| // edge of the close button. |
| gfx::Rect divider(close_button_->x(), close_button_->y(), |
| button_separator_->width(), close_button_->height()); |
| canvas->DrawImageInt(*button_separator_, |
| view->GetMirroredXForRect(divider), |
| close_button_->y()); |
| |
| // We don't need the extra lightness in the edges when we're at the top edge |
| // of the screen or when the header's corners are not rounded. |
| // |
| // TODO(sky): this isn't quite right. What we really want is a method that |
| // returns bounds ignoring transforms on certain windows (such as workspaces) |
| // and is relative to the root. |
| if (frame_->GetNativeWindow()->bounds().y() == 0 || corner_radius == 0) |
| return; |
| |
| // Draw the top corners and edge. |
| int top_left_height = top_left_corner_->height(); |
| canvas->DrawImageInt(*top_left_corner_, |
| 0, 0, top_left_corner_->width(), top_left_height, |
| 0, 0, top_left_corner_->width(), top_left_height, |
| false); |
| canvas->TileImageInt(*top_edge_, |
| top_left_corner_->width(), |
| 0, |
| view->width() - top_left_corner_->width() - top_right_corner_->width(), |
| top_edge_->height()); |
| int top_right_height = top_right_corner_->height(); |
| canvas->DrawImageInt(*top_right_corner_, |
| 0, 0, |
| top_right_corner_->width(), top_right_height, |
| view->width() - top_right_corner_->width(), 0, |
| top_right_corner_->width(), top_right_height, |
| false); |
| |
| // Header left edge. |
| int header_left_height = theme_frame->height() - top_left_height; |
| canvas->TileImageInt(*header_left_edge_, |
| 0, top_left_height, |
| header_left_edge_->width(), header_left_height); |
| |
| // Header right edge. |
| int header_right_height = theme_frame->height() - top_right_height; |
| canvas->TileImageInt(*header_right_edge_, |
| view->width() - header_right_edge_->width(), |
| top_right_height, |
| header_right_edge_->width(), |
| header_right_height); |
| |
| // We don't draw edges around the content area. Web content goes flush |
| // to the edge of the window. |
| } |
| |
| void FramePainter::PaintHeaderContentSeparator(views::NonClientFrameView* view, |
| gfx::Canvas* canvas) { |
| // Paint the line just above the content area. |
| gfx::Rect client_bounds = view->GetBoundsForClientView(); |
| canvas->FillRect(gfx::Rect(client_bounds.x(), |
| client_bounds.y() - kHeaderContentSeparatorSize, |
| client_bounds.width(), |
| kHeaderContentSeparatorSize), |
| kHeaderContentSeparatorColor); |
| } |
| |
| int FramePainter::HeaderContentSeparatorSize() const { |
| return kHeaderContentSeparatorSize; |
| } |
| |
| void FramePainter::PaintTitleBar(views::NonClientFrameView* view, |
| gfx::Canvas* canvas, |
| const gfx::Font& title_font) { |
| // The window icon is painted by its own views::View. |
| views::WidgetDelegate* delegate = frame_->widget_delegate(); |
| if (delegate && delegate->ShouldShowWindowTitle()) { |
| gfx::Rect title_bounds = GetTitleBounds(title_font); |
| SkColor title_color = frame_->IsMaximized() ? |
| kMaximizedWindowTitleTextColor : kNonMaximizedWindowTitleTextColor; |
| canvas->DrawStringInt(delegate->GetWindowTitle(), |
| title_font, |
| title_color, |
| view->GetMirroredXForRect(title_bounds), |
| title_bounds.y(), |
| title_bounds.width(), |
| title_bounds.height(), |
| gfx::Canvas::NO_SUBPIXEL_RENDERING); |
| } |
| } |
| |
| void FramePainter::LayoutHeader(views::NonClientFrameView* view, |
| bool shorter_layout) { |
| // The new assets only make sense if the window is actually maximized or |
| // fullscreen. |
| if (shorter_layout && |
| (frame_->IsMaximized() || frame_->IsFullscreen()) && |
| GetTrackedByWorkspace(frame_->GetNativeWindow())) { |
| SetButtonImages(close_button_, |
| IDR_AURA_WINDOW_MAXIMIZED_CLOSE2, |
| IDR_AURA_WINDOW_MAXIMIZED_CLOSE2_H, |
| IDR_AURA_WINDOW_MAXIMIZED_CLOSE2_P); |
| // The chat window cannot be restored but only minimized. |
| if (size_button_behavior_ == SIZE_BUTTON_MINIMIZES) { |
| SetButtonImages(size_button_, |
| IDR_AURA_WINDOW_MINIMIZE_SHORT, |
| IDR_AURA_WINDOW_MINIMIZE_SHORT_H, |
| IDR_AURA_WINDOW_MINIMIZE_SHORT_P); |
| } else { |
| SetButtonImages(size_button_, |
| IDR_AURA_WINDOW_MAXIMIZED_RESTORE2, |
| IDR_AURA_WINDOW_MAXIMIZED_RESTORE2_H, |
| IDR_AURA_WINDOW_MAXIMIZED_RESTORE2_P); |
| } |
| } else if (shorter_layout) { |
| SetButtonImages(close_button_, |
| IDR_AURA_WINDOW_MAXIMIZED_CLOSE, |
| IDR_AURA_WINDOW_MAXIMIZED_CLOSE_H, |
| IDR_AURA_WINDOW_MAXIMIZED_CLOSE_P); |
| // The chat window cannot be restored but only minimized. |
| if (size_button_behavior_ == SIZE_BUTTON_MINIMIZES) { |
| SetButtonImages(size_button_, |
| IDR_AURA_WINDOW_MINIMIZE_SHORT, |
| IDR_AURA_WINDOW_MINIMIZE_SHORT_H, |
| IDR_AURA_WINDOW_MINIMIZE_SHORT_P); |
| } else { |
| SetButtonImages(size_button_, |
| IDR_AURA_WINDOW_MAXIMIZED_RESTORE, |
| IDR_AURA_WINDOW_MAXIMIZED_RESTORE_H, |
| IDR_AURA_WINDOW_MAXIMIZED_RESTORE_P); |
| } |
| } else { |
| SetButtonImages(close_button_, |
| IDR_AURA_WINDOW_CLOSE, |
| IDR_AURA_WINDOW_CLOSE_H, |
| IDR_AURA_WINDOW_CLOSE_P); |
| SetButtonImages(size_button_, |
| IDR_AURA_WINDOW_MAXIMIZE, |
| IDR_AURA_WINDOW_MAXIMIZE_H, |
| IDR_AURA_WINDOW_MAXIMIZE_P); |
| } |
| |
| gfx::Size close_size = close_button_->GetPreferredSize(); |
| close_button_->SetBounds( |
| view->width() - close_size.width() - kCloseButtonOffsetX, |
| kCloseButtonOffsetY, |
| close_size.width(), |
| close_size.height()); |
| |
| gfx::Size size_button_size = size_button_->GetPreferredSize(); |
| size_button_->SetBounds( |
| close_button_->x() - size_button_size.width() - kSizeButtonOffsetX, |
| close_button_->y(), |
| size_button_size.width(), |
| size_button_size.height()); |
| |
| if (window_icon_) { |
| // Vertically center the window icon with respect to the close button. |
| int icon_offset_y = GetCloseButtonCenterY() - window_icon_->height() / 2; |
| window_icon_->SetBounds(kIconOffsetX, icon_offset_y, kIconSize, kIconSize); |
| } |
| } |
| |
| void FramePainter::SchedulePaintForTitle(const gfx::Font& title_font) { |
| frame_->non_client_view()->SchedulePaintInRect(GetTitleBounds(title_font)); |
| } |
| |
| void FramePainter::OnThemeChanged() { |
| // We do not cache the images for |previous_theme_frame_id_| and |
| // |previous_theme_frame_overlay_id_|. Changing the theme changes the images |
| // returned from ui::ThemeProvider for |previous_theme_frame_id_| |
| // and |previous_theme_frame_overlay_id_|. Reset the image ids to prevent |
| // starting a crossfade animation with these images. |
| previous_theme_frame_id_ = 0; |
| previous_theme_frame_overlay_id_ = 0; |
| |
| if (crossfade_animation_.get() && crossfade_animation_->is_animating()) { |
| crossfade_animation_.reset(); |
| frame_->non_client_view()->SchedulePaintInRect(header_frame_bounds_); |
| } |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // aura::WindowObserver overrides: |
| |
| void FramePainter::OnWindowPropertyChanged(aura::Window* window, |
| const void* key, |
| intptr_t old) { |
| // When 'kWindowTrackedByWorkspaceKey' changes, we are going to paint the |
| // header differently. Schedule a paint to ensure everything is updated |
| // correctly. |
| if (key == internal::kWindowTrackedByWorkspaceKey && |
| GetTrackedByWorkspace(window)) { |
| frame_->non_client_view()->SchedulePaint(); |
| } |
| |
| if (key != aura::client::kShowStateKey) |
| return; |
| |
| // Maximized and fullscreen windows don't want resize handles overlapping the |
| // content area, because when the user moves the cursor to the right screen |
| // edge we want them to be able to hit the scroll bar. |
| if (ash::wm::IsWindowMaximized(window) || |
| ash::wm::IsWindowFullscreen(window)) { |
| window->set_hit_test_bounds_override_inner(gfx::Insets()); |
| } else { |
| window->set_hit_test_bounds_override_inner( |
| gfx::Insets(kResizeInsideBoundsSize, kResizeInsideBoundsSize, |
| kResizeInsideBoundsSize, kResizeInsideBoundsSize)); |
| } |
| } |
| |
| void FramePainter::OnWindowVisibilityChanged(aura::Window* window, |
| bool visible) { |
| // OnWindowVisibilityChanged can be called for the child windows of |window_|. |
| if (window != window_) |
| return; |
| |
| // Window visibility change may trigger the change of window solo-ness in a |
| // different window. |
| UpdateSoloWindowInRoot(window_->GetRootWindow(), visible ? NULL : window_); |
| } |
| |
| void FramePainter::OnWindowDestroying(aura::Window* destroying) { |
| DCHECK_EQ(window_, destroying); |
| |
| // Must be removed here and not in the destructor, as the aura::Window is |
| // already destroyed when our destructor runs. |
| window_->RemoveObserver(this); |
| |
| // If we have two or more windows open and we close this one, we might trigger |
| // the solo window appearance for another window. |
| UpdateSoloWindowInRoot(window_->GetRootWindow(), window_); |
| |
| window_ = NULL; |
| } |
| |
| void FramePainter::OnWindowBoundsChanged(aura::Window* window, |
| const gfx::Rect& old_bounds, |
| const gfx::Rect& new_bounds) { |
| // TODO(sky): this isn't quite right. What we really want is a method that |
| // returns bounds ignoring transforms on certain windows (such as workspaces). |
| if ((!frame_->IsMaximized() && !frame_->IsFullscreen()) && |
| ((old_bounds.y() == 0 && new_bounds.y() != 0) || |
| (old_bounds.y() != 0 && new_bounds.y() == 0))) { |
| SchedulePaintForHeader(); |
| } |
| } |
| |
| void FramePainter::OnWindowAddedToRootWindow(aura::Window* window) { |
| // Needs to trigger the window appearance change if the window moves across |
| // root windows and a solo window is already in the new root. |
| UpdateSoloWindowInRoot(window->GetRootWindow(), NULL /* ignore_window */); |
| } |
| |
| void FramePainter::OnWindowRemovingFromRootWindow(aura::Window* window) { |
| // Needs to trigger the window appearance change if the window moves across |
| // root windows and only one window is left in the previous root. Because |
| // |window| is not yet moved, |window| has to be ignored. |
| UpdateSoloWindowInRoot(window->GetRootWindow(), window); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // ui::AnimationDelegate overrides: |
| |
| void FramePainter::AnimationProgressed(const ui::Animation* animation) { |
| frame_->non_client_view()->SchedulePaintInRect(header_frame_bounds_); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // FramePainter, private: |
| |
| void FramePainter::SetButtonImages(views::ImageButton* button, |
| int normal_image_id, |
| int hot_image_id, |
| int pushed_image_id) { |
| ui::ThemeProvider* theme_provider = frame_->GetThemeProvider(); |
| button->SetImage(views::CustomButton::STATE_NORMAL, |
| theme_provider->GetImageSkiaNamed(normal_image_id)); |
| button->SetImage(views::CustomButton::STATE_HOVERED, |
| theme_provider->GetImageSkiaNamed(hot_image_id)); |
| button->SetImage(views::CustomButton::STATE_PRESSED, |
| theme_provider->GetImageSkiaNamed(pushed_image_id)); |
| } |
| |
| void FramePainter::SetToggledButtonImages(views::ToggleImageButton* button, |
| int normal_image_id, |
| int hot_image_id, |
| int pushed_image_id) { |
| ui::ThemeProvider* theme_provider = frame_->GetThemeProvider(); |
| button->SetToggledImage(views::CustomButton::STATE_NORMAL, |
| theme_provider->GetImageSkiaNamed(normal_image_id)); |
| button->SetToggledImage(views::CustomButton::STATE_HOVERED, |
| theme_provider->GetImageSkiaNamed(hot_image_id)); |
| button->SetToggledImage(views::CustomButton::STATE_PRESSED, |
| theme_provider->GetImageSkiaNamed(pushed_image_id)); |
| } |
| |
| int FramePainter::GetTitleOffsetX() const { |
| return window_icon_ ? |
| window_icon_->bounds().right() + kTitleIconOffsetX : |
| kTitleNoIconOffsetX; |
| } |
| |
| int FramePainter::GetCloseButtonCenterY() const { |
| return close_button_->y() + close_button_->height() / 2; |
| } |
| |
| int FramePainter::GetHeaderCornerRadius() const { |
| // Use square corners for maximized and fullscreen windows when they are |
| // tracked by the workspace code. (Windows which are not tracked by the |
| // workspace code are used for tab dragging.) |
| bool square_corners = ((frame_->IsMaximized() || frame_->IsFullscreen())) && |
| GetTrackedByWorkspace(frame_->GetNativeWindow()); |
| const int kCornerRadius = 2; |
| return square_corners ? 0 : kCornerRadius; |
| } |
| |
| int FramePainter::GetHeaderOpacity( |
| HeaderMode header_mode, |
| int theme_frame_id, |
| int theme_frame_overlay_id) const { |
| // User-provided themes are painted fully opaque. |
| ui::ThemeProvider* theme_provider = frame_->GetThemeProvider(); |
| if (theme_provider->HasCustomImage(theme_frame_id) || |
| (theme_frame_overlay_id != 0 && |
| theme_provider->HasCustomImage(theme_frame_overlay_id))) { |
| return kFullyOpaque; |
| } |
| |
| // The header is fully opaque when using the minimalistic header style. |
| if (ShouldUseMinimalHeaderStyle(THEMED_NO)) |
| return kFullyOpaque; |
| |
| // Single browser window is very transparent. |
| if (UseSoloWindowHeader()) |
| return kSoloWindowOpacity; |
| |
| // Otherwise, change transparency based on window activation status. |
| if (header_mode == ACTIVE) |
| return kActiveWindowOpacity; |
| return kInactiveWindowOpacity; |
| } |
| |
| bool FramePainter::UseSoloWindowHeader() const { |
| // Don't use transparent headers for panels, pop-ups, etc. |
| if (!IsSoloWindowHeaderCandidate(window_)) |
| return false; |
| aura::RootWindow* root = window_->GetRootWindow(); |
| if (!root || root->GetProperty(internal::kIgnoreSoloWindowFramePainterPolicy)) |
| return false; |
| // Don't recompute every time, as it would require many window property |
| // lookups. |
| return root->GetProperty(internal::kSoloWindowHeaderKey); |
| } |
| |
| // static |
| bool FramePainter::UseSoloWindowHeaderInRoot(RootWindow* root_window, |
| Window* ignore_window) { |
| int visible_window_count = 0; |
| std::vector<Window*> windows = GetWindowsForSoloHeaderUpdate(root_window); |
| for (std::vector<Window*>::const_iterator it = windows.begin(); |
| it != windows.end(); |
| ++it) { |
| Window* window = *it; |
| // Various sorts of windows "don't count" for this computation. |
| if (ignore_window == window || |
| !IsSoloWindowHeaderCandidate(window) || |
| !IsVisibleToRoot(window)) |
| continue; |
| if (wm::IsWindowMaximized(window)) |
| return false; |
| ++visible_window_count; |
| if (visible_window_count > 1) |
| return false; |
| } |
| // Count must be tested because all windows might be "don't count" windows |
| // in the loop above. |
| return visible_window_count == 1; |
| } |
| |
| // static |
| void FramePainter::UpdateSoloWindowInRoot(RootWindow* root, |
| Window* ignore_window) { |
| #if defined(OS_WIN) |
| // Non-Ash Windows doesn't do solo-window counting for transparency effects, |
| // as the desktop background and window frames are managed by the OS. |
| if (!ash::Shell::HasInstance()) |
| return; |
| #endif |
| if (!root) |
| return; |
| bool old_solo_header = root->GetProperty(internal::kSoloWindowHeaderKey); |
| bool new_solo_header = UseSoloWindowHeaderInRoot(root, ignore_window); |
| if (old_solo_header == new_solo_header) |
| return; |
| root->SetProperty(internal::kSoloWindowHeaderKey, new_solo_header); |
| // Invalidate all the window frames in the desktop. There should only be |
| // a few. |
| std::vector<Window*> windows = GetWindowsForSoloHeaderUpdate(root); |
| for (std::vector<Window*>::const_iterator it = windows.begin(); |
| it != windows.end(); |
| ++it) { |
| Widget* widget = Widget::GetWidgetForNativeWindow(*it); |
| if (widget && widget->non_client_view()) |
| widget->non_client_view()->SchedulePaint(); |
| } |
| } |
| |
| void FramePainter::SchedulePaintForHeader() { |
| int top_left_height = top_left_corner_->height(); |
| int top_right_height = top_right_corner_->height(); |
| frame_->non_client_view()->SchedulePaintInRect( |
| gfx::Rect(0, 0, frame_->non_client_view()->width(), |
| std::max(top_left_height, top_right_height))); |
| } |
| |
| gfx::Rect FramePainter::GetTitleBounds(const gfx::Font& title_font) { |
| int title_x = GetTitleOffsetX(); |
| // Center the text with respect to the close button. This way it adapts to |
| // the caption height and aligns exactly with the window icon. Don't use |
| // |window_icon_| for this computation as it may be NULL. |
| int title_y = GetCloseButtonCenterY() - title_font.GetHeight() / 2; |
| return gfx::Rect( |
| title_x, |
| std::max(0, title_y), |
| std::max(0, size_button_->x() - kTitleLogoSpacing - title_x), |
| title_font.GetHeight()); |
| } |
| |
| } // namespace ash |