// Copyright 2020 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/display/display_alignment_controller.h" #include "ash/display/display_alignment_indicator.h" #include "ash/session/session_controller_impl.h" #include "ash/shell.h" #include "base/bind.h" #include "base/callback.h" #include "base/timer/timer.h" #include "ui/events/event.h" #include "ui/gfx/color_palette.h" #include "ui/gfx/geometry/rect.h" namespace ash { namespace { // Number of times the mouse has to hit the edge to show the indicators. constexpr int kTriggerThresholdCount = 2; // Time between last time the mouse leaves a screen edge and the counter // resetting. constexpr base::TimeDelta kCounterResetTime = base::Seconds(1); // How long the indicators are visible for. constexpr base::TimeDelta kIndicatorVisibilityDuration = base::Seconds(2); // Returns true if |screen_location| is on the edge of |display|. |display| must // be valid. bool IsOnBoundary(const gfx::Point& screen_location, const display::Display& display) { DCHECK(display.is_valid()); const gfx::Rect& bounds = display.bounds(); const int top = bounds.y(); const int bottom = bounds.bottom() - 1; const int left = bounds.x(); const int right = bounds.right() - 1; // See if current screen_location is within 1px of the display's // borders. 1px leniency is necessary as some resolution/size factor // combination results in the mouse not being able to reach the edges of the // display by 1px. if (std::abs(screen_location.x() - left) <= 1) return true; if (std::abs(screen_location.x() - right) <= 1) return true; if (std::abs(screen_location.y() - top) <= 1) return true; return std::abs(screen_location.y() - bottom) <= 1; } } // namespace DisplayAlignmentController::DisplayAlignmentController() : action_trigger_timer_(std::make_unique()) { Shell* shell = Shell::Get(); shell->AddPreTargetHandler(this); shell->session_controller()->AddObserver(this); shell->window_tree_host_manager()->AddObserver(this); is_locked_ = shell->session_controller()->IsScreenLocked(); RefreshState(); } DisplayAlignmentController::~DisplayAlignmentController() { Shell* shell = Shell::Get(); shell->window_tree_host_manager()->RemoveObserver(this); shell->session_controller()->RemoveObserver(this); shell->RemovePreTargetHandler(this); } void DisplayAlignmentController::OnDisplayConfigurationChanged() { RefreshState(); } void DisplayAlignmentController::OnDisplaysInitialized() { RefreshState(); } void DisplayAlignmentController::OnMouseEvent(ui::MouseEvent* event) { if (current_state_ == DisplayAlignmentState::kDisabled || event->type() != ui::ET_MOUSE_MOVED) { return; } // If mouse enters the edge of the display. const gfx::Point screen_location = event->target()->GetScreenLocation(*event); const display::Display& src_display = Shell::Get()->display_manager()->FindDisplayContainingPoint( screen_location); if (!src_display.is_valid()) return; const bool is_on_edge = IsOnBoundary(screen_location, src_display); // Restart the reset timer when the mouse moves off an edge. if (!is_on_edge) { if (current_state_ == DisplayAlignmentState::kOnEdge) { current_state_ = DisplayAlignmentState::kIdle; // The cursor was moved off the edge. Start the reset timer. If the cursor // does not hit an edge on the same display within |kCounterResetTime|, // state is reset by ResetState() and indicators will not be shown. action_trigger_timer_->Start( FROM_HERE, kCounterResetTime, base::BindOnce(&DisplayAlignmentController::ResetState, base::Unretained(this))); } return; } if (current_state_ != DisplayAlignmentState::kIdle) return; // |trigger_count_| should only increment when the mouse hits the edges of // the same display. if (triggered_display_id_ == src_display.id()) { trigger_count_++; } else { triggered_display_id_ = src_display.id(); trigger_count_ = 1; } action_trigger_timer_->Stop(); current_state_ = DisplayAlignmentState::kOnEdge; if (trigger_count_ == kTriggerThresholdCount) ShowIndicators(src_display); } void DisplayAlignmentController::OnLockStateChanged(const bool locked) { is_locked_ = locked; RefreshState(); } void DisplayAlignmentController::DisplayDragged(int64_t display_id, int32_t delta_x, int32_t delta_y) { if (current_state_ != DisplayAlignmentState::kLayoutPreview) { // Clear existing indicators. They are all regenerated via // OnDisplayConfigurationChanged() after dragging ends. ResetState(); dragged_display_id_ = display_id; current_state_ = DisplayAlignmentState::kLayoutPreview; } // It is not possible to change display being dragged without dropping it // first (and causing update on display configuration). DCHECK_EQ(dragged_display_id_, display_id); DCHECK_NE(dragged_display_id_, display::kInvalidDisplayId); dragged_offset_ += gfx::Vector2d(delta_x, delta_y); ComputePreviewIndicators(); } void DisplayAlignmentController::SetTimerForTesting( std::unique_ptr timer) { action_trigger_timer_ = std::move(timer); } const std::vector>& DisplayAlignmentController::GetActiveIndicatorsForTesting() { return active_indicators_; } int64_t DisplayAlignmentController::GetDraggedDisplayIdForTesting() const { return dragged_display_id_; } void DisplayAlignmentController::ShowIndicators( const display::Display& src_display) { DCHECK_EQ(src_display.id(), triggered_display_id_); current_state_ = DisplayAlignmentState::kIndicatorsVisible; // Iterate through all the active displays and see if they are neighbors to // |src_display|. display::DisplayManager* display_manager = Shell::Get()->display_manager(); const display::Displays& display_list = display_manager->active_display_list(); for (const display::Display& peer : display_list) { // Skip currently triggered display or it might be detected as a neighbor if (peer.id() == triggered_display_id_) continue; // Check whether |src_display| and |peer| are neighbors. gfx::Rect source_edge; gfx::Rect peer_edge; if (display::ComputeBoundary(src_display, peer, &source_edge, &peer_edge)) { // TODO(1070697): Handle pills overlapping for certain display // configuration. // Pills are created for the indicators in the src display, but not in the // peers. const std::string& dst_name = display_manager->GetDisplayInfo(peer.id()).name(); active_indicators_.push_back(DisplayAlignmentIndicator::CreateWithPill( src_display, source_edge, dst_name)); active_indicators_.push_back( DisplayAlignmentIndicator::Create(peer, peer_edge)); } } action_trigger_timer_->Start( FROM_HERE, kIndicatorVisibilityDuration, base::BindOnce(&DisplayAlignmentController::ResetState, base::Unretained(this))); } void DisplayAlignmentController::ResetState() { action_trigger_timer_->Stop(); active_indicators_.clear(); trigger_count_ = 0; dragged_display_id_ = display::kInvalidDisplayId; dragged_offset_ = gfx::Vector2d(0, 0); // Do not re-enable if disabled. if (current_state_ != DisplayAlignmentState::kDisabled) current_state_ = DisplayAlignmentState::kIdle; } void DisplayAlignmentController::RefreshState() { ResetState(); // This feature is only enabled when the screen is not locked and there is // more than one display connected. if (is_locked_) { current_state_ = DisplayAlignmentState::kDisabled; return; } const display::Displays& display_list = Shell::Get()->display_manager()->active_display_list(); if (display_list.size() < 2) { current_state_ = DisplayAlignmentState::kDisabled; return; } if (current_state_ == DisplayAlignmentState::kDisabled) current_state_ = DisplayAlignmentState::kIdle; } void DisplayAlignmentController::ComputePreviewIndicators() { DCHECK_EQ(current_state_, DisplayAlignmentState::kLayoutPreview); DCHECK_NE(dragged_display_id_, display::kInvalidDisplayId); const display::Display& dragged_display = Shell::Get()->display_manager()->GetDisplayForId(dragged_display_id_); DCHECK(dragged_display.is_valid()); gfx::Rect bounds = dragged_display.bounds(); bounds += dragged_offset_; const display::Displays& display_list = Shell::Get()->display_manager()->active_display_list(); // Iterate through all the active displays and see if they are neighbors to // |dragged_display|. for (const display::Display& peer : display_list) { // Skip currently dragged display or it might be detected as a neighbor if (peer.id() == dragged_display_id_) continue; // True if |source| and |peer| are neighbors. Returns |source_edge| and // |peer_edge| that denotes shared edges between |source| and |peer| // displays. gfx::Rect source_edge; gfx::Rect peer_edge; const bool are_neighbors = display::ComputeBoundary( bounds, peer.bounds(), &source_edge, &peer_edge); const auto& existing_indicator_it = std::find_if(active_indicators_.begin(), active_indicators_.end(), [id = peer.id()](const auto& indicator) { return id == indicator->display_id(); }); const bool indicator_exists = existing_indicator_it != active_indicators_.end(); if (indicator_exists) { if (are_neighbors) { // Displays are already neighbors. (*existing_indicator_it)->Update(peer, peer_edge); (*existing_indicator_it)->Show(); } else { // Displays are no longer neighbors but previously were neighbors. (*existing_indicator_it)->Hide(); } } else if (are_neighbors) { // Displays are newly-neighbored. active_indicators_.push_back( DisplayAlignmentIndicator::Create(peer, peer_edge)); } } } } // namespace ash