// 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 "ash/wm/default_state.h" #include "ash/display/display_controller.h" #include "ash/screen_util.h" #include "ash/shell.h" #include "ash/shell_window_ids.h" #include "ash/wm/coordinate_conversion.h" #include "ash/wm/window_animations.h" #include "ash/wm/window_state.h" #include "ash/wm/window_state_delegate.h" #include "ash/wm/window_state_util.h" #include "ash/wm/window_util.h" #include "ash/wm/wm_event.h" #include "ash/wm/workspace/workspace_window_resizer.h" #include "ui/aura/client/aura_constants.h" #include "ui/aura/window.h" #include "ui/aura/window_delegate.h" #include "ui/gfx/display.h" #include "ui/gfx/rect.h" namespace ash { namespace wm { namespace { // This specifies how much percent (30%) of a window rect // must be visible when the window is added to the workspace. const float kMinimumPercentOnScreenArea = 0.3f; bool IsPanel(aura::Window* window) { return window->parent() && window->parent()->id() == kShellWindowId_PanelContainer; } void MoveToDisplayForRestore(WindowState* window_state) { if (!window_state->HasRestoreBounds()) return; const gfx::Rect& restore_bounds = window_state->GetRestoreBoundsInScreen(); // Move only if the restore bounds is outside of // the display. There is no information about in which // display it should be restored, so this is best guess. // TODO(oshima): Restore information should contain the // work area information like WindowResizer does for the // last window location. gfx::Rect display_area = Shell::GetScreen()->GetDisplayNearestWindow( window_state->window()).bounds(); if (!display_area.Intersects(restore_bounds)) { const gfx::Display& display = Shell::GetScreen()->GetDisplayMatching(restore_bounds); DisplayController* display_controller = Shell::GetInstance()->display_controller(); aura::Window* new_root = display_controller->GetRootWindowForDisplayId(display.id()); if (new_root != window_state->window()->GetRootWindow()) { aura::Window* new_container = Shell::GetContainer(new_root, window_state->window()->parent()->id()); new_container->AddChild(window_state->window()); } } } } // namespace; DefaultState::DefaultState(WindowStateType initial_state_type) : state_type_(initial_state_type) {} DefaultState::~DefaultState() {} void DefaultState::OnWMEvent(WindowState* window_state, const WMEvent* event) { if (ProcessWorkspaceEvents(window_state, event)) return; if (ProcessCompoundEvents(window_state, event)) return; WindowStateType next_state_type = WINDOW_STATE_TYPE_NORMAL; switch (event->type()) { case WM_EVENT_NORMAL: next_state_type = WINDOW_STATE_TYPE_NORMAL; break; case WM_EVENT_MAXIMIZE: next_state_type = WINDOW_STATE_TYPE_MAXIMIZED; break; case WM_EVENT_MINIMIZE: next_state_type = WINDOW_STATE_TYPE_MINIMIZED; break; case WM_EVENT_FULLSCREEN: next_state_type = WINDOW_STATE_TYPE_FULLSCREEN; break; case WM_EVENT_SNAP_LEFT: next_state_type = WINDOW_STATE_TYPE_LEFT_SNAPPED; break; case WM_EVENT_SNAP_RIGHT: next_state_type = WINDOW_STATE_TYPE_RIGHT_SNAPPED; break; case WM_EVENT_SET_BOUNDS: SetBounds(window_state, static_cast<const SetBoundsEvent*>(event)); return; case WM_EVENT_SHOW_INACTIVE: next_state_type = WINDOW_STATE_TYPE_INACTIVE; break; case WM_EVENT_TOGGLE_MAXIMIZE_CAPTION: case WM_EVENT_TOGGLE_MAXIMIZE: case WM_EVENT_TOGGLE_VERTICAL_MAXIMIZE: case WM_EVENT_TOGGLE_HORIZONTAL_MAXIMIZE: case WM_EVENT_TOGGLE_FULLSCREEN: case WM_EVENT_CENTER: NOTREACHED() << "Compound event should not reach here:" << event; return; case WM_EVENT_ADDED_TO_WORKSPACE: case WM_EVENT_WORKAREA_BOUNDS_CHANGED: case WM_EVENT_DISPLAY_BOUNDS_CHANGED: NOTREACHED() << "Workspace event should not reach here:" << event; return; } WindowStateType current = window_state->GetStateType(); if (next_state_type == current && window_state->IsSnapped()) { gfx::Rect snapped_bounds = event->type() == WM_EVENT_SNAP_LEFT ? GetDefaultLeftSnappedWindowBoundsInParent(window_state->window()) : GetDefaultRightSnappedWindowBoundsInParent(window_state->window()); window_state->SetBoundsDirectAnimated(snapped_bounds); return; } EnterToNextState(window_state, next_state_type); } WindowStateType DefaultState::GetType() const { return state_type_; } void DefaultState::AttachState( WindowState* window_state, WindowState::State* state_in_previous_mode) { DCHECK_EQ(stored_window_state_, window_state); ReenterToCurrentState(window_state, state_in_previous_mode); // If the display has changed while in the another mode, // we need to let windows know the change. gfx::Display current_display = Shell::GetScreen()-> GetDisplayNearestWindow(window_state->window()); if (stored_display_state_.bounds() != current_display.bounds()) { const WMEvent event(wm::WM_EVENT_DISPLAY_BOUNDS_CHANGED); window_state->OnWMEvent(&event); } else if (stored_display_state_.work_area() != current_display.work_area()) { const WMEvent event(wm::WM_EVENT_WORKAREA_BOUNDS_CHANGED); window_state->OnWMEvent(&event); } } void DefaultState::DetachState(WindowState* window_state) { stored_window_state_ = window_state; aura::Window* window = window_state->window(); stored_bounds_ = window->bounds(); stored_restore_bounds_ = window_state->HasRestoreBounds() ? window_state->GetRestoreBoundsInParent() : gfx::Rect(); // Remember the display state so that in case of the display change // while in the other mode, we can perform necessary action to // restore the window state to the proper state for the current // display. stored_display_state_ = Shell::GetScreen()-> GetDisplayNearestWindow(window_state->window()); } // static bool DefaultState::ProcessCompoundEvents(WindowState* window_state, const WMEvent* event) { aura::Window* window = window_state->window(); switch (event->type()) { case WM_EVENT_TOGGLE_MAXIMIZE_CAPTION: if (window_state->IsFullscreen()) { const wm::WMEvent event(wm::WM_EVENT_TOGGLE_FULLSCREEN); window_state->OnWMEvent(&event); } else if (window_state->IsMaximized()) { window_state->Restore(); } else if (window_state->IsNormalOrSnapped()) { if (window_state->CanMaximize()) window_state->Maximize(); } return true; case WM_EVENT_TOGGLE_MAXIMIZE: if (window_state->IsFullscreen()) { const wm::WMEvent event(wm::WM_EVENT_TOGGLE_FULLSCREEN); window_state->OnWMEvent(&event); } else if (window_state->IsMaximized()) { window_state->Restore(); } else if (window_state->CanMaximize()) { window_state->Maximize(); } return true; case WM_EVENT_TOGGLE_VERTICAL_MAXIMIZE: { gfx::Rect work_area = ScreenUtil::GetDisplayWorkAreaBoundsInParent(window); // Maximize vertically if: // - The window does not have a max height defined. // - The window has the normal state type. Snapped windows are excluded // because they are already maximized vertically and reverting to the // restored bounds looks weird. if (window->delegate()->GetMaximumSize().height() != 0 || !window_state->IsNormalStateType()) { return true; } if (window_state->HasRestoreBounds() && (window->bounds().height() == work_area.height() && window->bounds().y() == work_area.y())) { window_state->SetAndClearRestoreBounds(); } else { window_state->SaveCurrentBoundsForRestore(); window->SetBounds(gfx::Rect(window->bounds().x(), work_area.y(), window->bounds().width(), work_area.height())); } return true; } case WM_EVENT_TOGGLE_HORIZONTAL_MAXIMIZE: { // Maximize horizontally if: // - The window does not have a max width defined. // - The window is snapped or has the normal state type. if (window->delegate()->GetMaximumSize().width() != 0) return true; if (!window_state->IsNormalOrSnapped()) return true; gfx::Rect work_area = ScreenUtil::GetDisplayWorkAreaBoundsInParent(window); if (window_state->IsNormalStateType() && window_state->HasRestoreBounds() && (window->bounds().width() == work_area.width() && window->bounds().x() == work_area.x())) { window_state->SetAndClearRestoreBounds(); } else { gfx::Rect new_bounds(work_area.x(), window->bounds().y(), work_area.width(), window->bounds().height()); gfx::Rect restore_bounds = window->bounds(); if (window_state->IsSnapped()) { window_state->SetRestoreBoundsInParent(new_bounds); window_state->Restore(); // The restore logic prevents a window from being restored to bounds // which match the workspace bounds exactly so it is necessary to set // the bounds again below. } window_state->SetRestoreBoundsInParent(restore_bounds); window->SetBounds(new_bounds); } return true; } case WM_EVENT_TOGGLE_FULLSCREEN: ToggleFullScreen(window_state, window_state->delegate()); return true; case WM_EVENT_CENTER: CenterWindow(window_state); return true; case WM_EVENT_NORMAL: case WM_EVENT_MAXIMIZE: case WM_EVENT_MINIMIZE: case WM_EVENT_FULLSCREEN: case WM_EVENT_SNAP_LEFT: case WM_EVENT_SNAP_RIGHT: case WM_EVENT_SET_BOUNDS: case WM_EVENT_SHOW_INACTIVE: break; case WM_EVENT_ADDED_TO_WORKSPACE: case WM_EVENT_WORKAREA_BOUNDS_CHANGED: case WM_EVENT_DISPLAY_BOUNDS_CHANGED: NOTREACHED() << "Workspace event should not reach here:" << event; break; } return false; } bool DefaultState::ProcessWorkspaceEvents(WindowState* window_state, const WMEvent* event) { switch (event->type()) { case WM_EVENT_ADDED_TO_WORKSPACE: { // When a window is dragged and dropped onto a different // root window, the bounds will be updated after they are added // to the root window. // If a window is opened as maximized or fullscreen, its bounds may be // empty, so update the bounds now before checking empty. if (window_state->is_dragged() || SetMaximizedOrFullscreenBounds(window_state)) { return true; } aura::Window* window = window_state->window(); gfx::Rect bounds = window->bounds(); // Don't adjust window bounds if the bounds are empty as this // happens when a new views::Widget is created. if (bounds.IsEmpty()) return true; // Use entire display instead of workarea because the workarea can // be further shrunk by the docked area. The logic ensures 30% // visibility which should be enough to see where the window gets // moved. gfx::Rect display_area = ScreenUtil::GetDisplayBoundsInParent(window); int min_width = bounds.width() * kMinimumPercentOnScreenArea; int min_height = bounds.height() * kMinimumPercentOnScreenArea; AdjustBoundsToEnsureWindowVisibility( display_area, min_width, min_height, &bounds); window_state->AdjustSnappedBounds(&bounds); if (window->bounds() != bounds) window_state->SetBoundsConstrained(bounds); return true; } case WM_EVENT_DISPLAY_BOUNDS_CHANGED: { if (window_state->is_dragged() || SetMaximizedOrFullscreenBounds(window_state)) { return true; } gfx::Rect work_area_in_parent = ScreenUtil::GetDisplayWorkAreaBoundsInParent(window_state->window()); gfx::Rect bounds = window_state->window()->bounds(); // When display bounds has changed, make sure the entire window is fully // visible. bounds.AdjustToFit(work_area_in_parent); window_state->AdjustSnappedBounds(&bounds); if (window_state->window()->bounds() != bounds) window_state->SetBoundsDirectAnimated(bounds); return true; } case WM_EVENT_WORKAREA_BOUNDS_CHANGED: { if (window_state->is_dragged() || SetMaximizedOrFullscreenBounds(window_state)) { return true; } gfx::Rect work_area_in_parent = ScreenUtil::GetDisplayWorkAreaBoundsInParent(window_state->window()); gfx::Rect bounds = window_state->window()->bounds(); AdjustBoundsToEnsureMinimumWindowVisibility(work_area_in_parent, &bounds); window_state->AdjustSnappedBounds(&bounds); if (window_state->window()->bounds() != bounds) window_state->SetBoundsDirectAnimated(bounds); return true; } case WM_EVENT_TOGGLE_MAXIMIZE_CAPTION: case WM_EVENT_TOGGLE_MAXIMIZE: case WM_EVENT_TOGGLE_VERTICAL_MAXIMIZE: case WM_EVENT_TOGGLE_HORIZONTAL_MAXIMIZE: case WM_EVENT_TOGGLE_FULLSCREEN: case WM_EVENT_CENTER: case WM_EVENT_NORMAL: case WM_EVENT_MAXIMIZE: case WM_EVENT_MINIMIZE: case WM_EVENT_FULLSCREEN: case WM_EVENT_SNAP_LEFT: case WM_EVENT_SNAP_RIGHT: case WM_EVENT_SET_BOUNDS: case WM_EVENT_SHOW_INACTIVE: break; } return false; } // static bool DefaultState::SetMaximizedOrFullscreenBounds(WindowState* window_state) { DCHECK(!window_state->is_dragged()); if (window_state->IsMaximized()) { window_state->SetBoundsDirect( ScreenUtil::GetMaximizedWindowBoundsInParent(window_state->window())); return true; } if (window_state->IsFullscreen()) { window_state->SetBoundsDirect( ScreenUtil::GetDisplayBoundsInParent(window_state->window())); return true; } return false; } // static void DefaultState::SetBounds(WindowState* window_state, const SetBoundsEvent* event) { if (window_state->is_dragged()) { window_state->SetBoundsDirect(event->requested_bounds()); } else if (window_state->IsSnapped()) { gfx::Rect work_area_in_parent = ScreenUtil::GetDisplayWorkAreaBoundsInParent(window_state->window()); gfx::Rect child_bounds(event->requested_bounds()); AdjustBoundsSmallerThan(work_area_in_parent.size(), &child_bounds); window_state->AdjustSnappedBounds(&child_bounds); window_state->SetBoundsDirect(child_bounds); } else if (!SetMaximizedOrFullscreenBounds(window_state)) { window_state->SetBoundsConstrained(event->requested_bounds()); } } void DefaultState::EnterToNextState(WindowState* window_state, WindowStateType next_state_type) { // Do nothing if we're already in the same state. if (state_type_ == next_state_type) return; WindowStateType previous_state_type = state_type_; state_type_ = next_state_type; window_state->UpdateWindowShowStateFromStateType(); window_state->NotifyPreStateTypeChange(previous_state_type); // This Docked/Snapped hack is due to the issue that IsDocked returns // true for dragging window. TODO(oshima): Make docked window a state // and remove this hack. if (window_state->window()->parent() && (window_state->IsSnapped() || (!window_state->IsDocked() && !IsPanel(window_state->window())))) { if (!window_state->HasRestoreBounds() && (previous_state_type == WINDOW_STATE_TYPE_DEFAULT || previous_state_type == WINDOW_STATE_TYPE_NORMAL) && !window_state->IsMinimized() && !window_state->IsNormalStateType()) { window_state->SaveCurrentBoundsForRestore(); } // When restoring from a minimized state, we want to restore to the previous // bounds. However, we want to maintain the restore bounds. (The restore // bounds are set if a user maximized the window in one axis by double // clicking the window border for example). gfx::Rect restore_bounds_in_screen; if (previous_state_type == WINDOW_STATE_TYPE_MINIMIZED && window_state->IsNormalStateType() && window_state->HasRestoreBounds() && !window_state->unminimize_to_restore_bounds()) { restore_bounds_in_screen = window_state->GetRestoreBoundsInScreen(); window_state->SaveCurrentBoundsForRestore(); } if (window_state->IsMaximizedOrFullscreen()) MoveToDisplayForRestore(window_state); UpdateBoundsFromState(window_state, previous_state_type); // Normal state should have no restore bounds unless it's // unminimzied. if (!restore_bounds_in_screen.IsEmpty()) window_state->SetRestoreBoundsInScreen(restore_bounds_in_screen); else if (window_state->IsNormalStateType()) window_state->ClearRestoreBounds(); } window_state->NotifyPostStateTypeChange(previous_state_type); } void DefaultState::ReenterToCurrentState( WindowState* window_state, WindowState::State* state_in_previous_mode) { WindowStateType previous_state_type = state_in_previous_mode->GetType(); if (previous_state_type == wm::WINDOW_STATE_TYPE_FULLSCREEN) { // A state change should not move a window out of full screen since full // screen is a "special mode" the user wanted to be in and should be // respected as such. state_type_ = wm::WINDOW_STATE_TYPE_FULLSCREEN; } window_state->UpdateWindowShowStateFromStateType(); window_state->NotifyPreStateTypeChange(previous_state_type); if ((state_type_ == wm::WINDOW_STATE_TYPE_NORMAL || state_type_ == wm::WINDOW_STATE_TYPE_DEFAULT) && !stored_bounds_.IsEmpty()) { // Use the restore mechanism to set the bounds for // the window in normal state. This also covers unminimize case. window_state->SetRestoreBoundsInParent(stored_bounds_); } UpdateBoundsFromState(window_state, state_in_previous_mode->GetType()); // Then restore the restore bounds to their previous value. if (!stored_restore_bounds_.IsEmpty()) window_state->SetRestoreBoundsInParent(stored_restore_bounds_); else window_state->ClearRestoreBounds(); window_state->NotifyPostStateTypeChange(previous_state_type); } void DefaultState::UpdateBoundsFromState(WindowState* window_state, WindowStateType previous_state_type) { aura::Window* window = window_state->window(); gfx::Rect bounds_in_parent; switch (state_type_) { case WINDOW_STATE_TYPE_LEFT_SNAPPED: case WINDOW_STATE_TYPE_RIGHT_SNAPPED: bounds_in_parent = state_type_ == WINDOW_STATE_TYPE_LEFT_SNAPPED ? GetDefaultLeftSnappedWindowBoundsInParent(window_state->window()) : GetDefaultRightSnappedWindowBoundsInParent(window_state->window()); break; case WINDOW_STATE_TYPE_DEFAULT: case WINDOW_STATE_TYPE_NORMAL: { gfx::Rect work_area_in_parent = ScreenUtil::GetDisplayWorkAreaBoundsInParent(window_state->window()); if (window_state->HasRestoreBounds()) bounds_in_parent = window_state->GetRestoreBoundsInParent(); else bounds_in_parent = window->bounds(); // Make sure that part of the window is always visible. AdjustBoundsToEnsureMinimumWindowVisibility( work_area_in_parent, &bounds_in_parent); break; } case WINDOW_STATE_TYPE_MAXIMIZED: bounds_in_parent = ScreenUtil::GetMaximizedWindowBoundsInParent(window); break; case WINDOW_STATE_TYPE_FULLSCREEN: bounds_in_parent = ScreenUtil::GetDisplayBoundsInParent(window); break; case WINDOW_STATE_TYPE_MINIMIZED: break; case WINDOW_STATE_TYPE_INACTIVE: case WINDOW_STATE_TYPE_DETACHED: case WINDOW_STATE_TYPE_END: case WINDOW_STATE_TYPE_AUTO_POSITIONED: return; } if (state_type_ != WINDOW_STATE_TYPE_MINIMIZED) { if (previous_state_type == WINDOW_STATE_TYPE_MINIMIZED || window_state->IsFullscreen()) { window_state->SetBoundsDirect(bounds_in_parent); } else if (window_state->IsMaximized() || IsMaximizedOrFullscreenWindowStateType(previous_state_type)) { window_state->SetBoundsDirectCrossFade(bounds_in_parent); } else if (window_state->is_dragged()) { // SetBoundsDirectAnimated does not work when the window gets reparented. // TODO(oshima): Consider fixing it and reenable the animation. window_state->SetBoundsDirect(bounds_in_parent); } else { window_state->SetBoundsDirectAnimated(bounds_in_parent); } } if (window_state->IsMinimized()) { // Save the previous show state so that we can correctly restore it. window_state->window()->SetProperty(aura::client::kRestoreShowStateKey, ToWindowShowState(previous_state_type)); ::wm::SetWindowVisibilityAnimationType( window_state->window(), WINDOW_VISIBILITY_ANIMATION_TYPE_MINIMIZE); // Hide the window. window_state->window()->Hide(); // Activate another window. if (window_state->IsActive()) window_state->Deactivate(); } else if ((window_state->window()->TargetVisibility() || previous_state_type == WINDOW_STATE_TYPE_MINIMIZED) && !window_state->window()->layer()->visible()) { // The layer may be hidden if the window was previously minimized. Make // sure it's visible. window_state->window()->Show(); if (previous_state_type == WINDOW_STATE_TYPE_MINIMIZED && !window_state->IsMaximizedOrFullscreen()) { window_state->set_unminimize_to_restore_bounds(false); } } } // static void DefaultState::CenterWindow(WindowState* window_state) { if (!window_state->IsNormalOrSnapped()) return; aura::Window* window = window_state->window(); if (window_state->IsSnapped()) { gfx::Rect center_in_screen = Shell::GetScreen()->GetDisplayNearestWindow(window).work_area(); gfx::Size size = window_state->HasRestoreBounds() ? window_state->GetRestoreBoundsInScreen().size() : window->bounds().size(); center_in_screen.ClampToCenteredSize(size); window_state->SetRestoreBoundsInScreen(center_in_screen); window_state->Restore(); } else { gfx::Rect center_in_parent = ScreenUtil::GetDisplayWorkAreaBoundsInParent(window); center_in_parent.ClampToCenteredSize(window->bounds().size()); window_state->SetBoundsDirectAnimated(center_in_parent); } // Centering window is treated as if a user moved and resized the window. window_state->set_bounds_changed_by_user(true); } } // namespace wm } // namespace ash