chromium/chromecast/graphics/cast_focus_client_aura.cc

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromecast/graphics/cast_focus_client_aura.h"

#include "base/containers/contains.h"
#include "base/logging.h"
#include "base/ranges/algorithm.h"
#include "ui/aura/window.h"

#define LOG_WINDOW_INFO(top_level, window)                                 \
  "top-level: " << (top_level)->GetId() << ": '" << (top_level)->GetName() \
                << "', window: " << (window)->GetId() << ": '"             \
                << (window)->GetName() << "'"

namespace chromecast {

CastFocusClientAura::CastFocusClientAura() : focused_window_(nullptr) {}

CastFocusClientAura::~CastFocusClientAura() {
  focused_window_ = nullptr;
  for (aura::Window* window : focusable_windows_) {
    window->RemoveObserver(this);
  }
  focusable_windows_.clear();
}

aura::Window* CastFocusClientAura::GetFocusedWindow() {
  return focused_window_;
}

void CastFocusClientAura::AddObserver(
    aura::client::FocusChangeObserver* observer) {
  focus_observers_.AddObserver(observer);
}

void CastFocusClientAura::RemoveObserver(
    aura::client::FocusChangeObserver* observer) {
  focus_observers_.RemoveObserver(observer);
}

void CastFocusClientAura::OnWindowVisibilityChanged(aura::Window* window,
                                                    bool visible) {
  if (!visible && (window == focused_window_)) {
    // The focused window just lost visibility, so de-focus it.
    UpdateWindowFocus();
  } else if (visible) {
    // The window that just became visible might be the most appropriate window
    // to have focus.
    UpdateWindowFocus();
  }
}

// One of our observed windows is being destroyed.
// We observe each window that has the potential for being focused,
// so this window needs to be removed from the list of focusable windows.
void CastFocusClientAura::OnWindowDestroying(aura::Window* window) {
  aura::Window* top_level = GetZOrderWindow(window);
  DCHECK(top_level);
  DLOG(INFO) << "Removing window, " << LOG_WINDOW_INFO(top_level, window);

  auto iter = base::ranges::find(focusable_windows_, window);
  if (iter != focusable_windows_.end()) {
    focusable_windows_.erase(iter);
    window->RemoveObserver(this);
  }
  if (window == focused_window_) {
    // De-focus the window that is being destroyed.
    UpdateWindowFocus();
  }
}

// Update focus if a window is entering or leaving our hierarchy.
void CastFocusClientAura::OnWindowHierarchyChanging(
    const HierarchyChangeParams& params) {
  if (params.new_parent &&
      (aura::client::GetFocusClient(params.new_parent) == this)) {
    if (params.old_parent == params.new_parent) {
      // A window is moving within our hierarchy.
      return;
    } else {
      // A window is entering our hierarchy, so we need to consider
      // focusing it.
      FocusWindow(params.target);
      return;
    }
  }

  // The window is leaving our hierarchy, so stop tracking it.
  // It could contain multiple windows that were focused, so lets stop tracking
  // them all.
  auto iter = focusable_windows_.begin();
  bool was_focused = false;
  while (iter != focusable_windows_.end()) {
    aura::Window* window = *iter;
    if (params.target == window || params.target->Contains(window)) {
      window->RemoveObserver(this);
      was_focused |= window == focused_window_;
      iter = focusable_windows_.erase(iter);

      aura::Window* top_level = GetZOrderWindow(window);
      DCHECK(top_level);
      DLOG(INFO) << "Dropping window, " << LOG_WINDOW_INFO(top_level, window);
    } else {
      ++iter;
    }
  }

  if (was_focused) {
    // The window that was removed from our hierarchy was the focused window, so
    // de-focus it.
    UpdateWindowFocus();
  }
}

// An explicit request to focus a window.
// We lock focus to the top-most high-level window, and so will ignore this
// focus request if it isn't for the topmost window.  If it is for a lower
// window, then we'll track it to focus it later when it rises to the top.
void CastFocusClientAura::FocusWindow(aura::Window* window) {
  if (window) {
    if (!window->CanFocus()) {
      return;
    }
    aura::Window* top_level = GetZOrderWindow(window);
    DCHECK(top_level);
    DLOG(INFO) << "Requesting focus for " << LOG_WINDOW_INFO(top_level, window);
    if (!base::Contains(focusable_windows_, window)) {
      // We're not yet tracking this focusable window, so start tracking it as a
      // potential focus target.
      window->AddObserver(this);
      focusable_windows_.push_back(window);
    }
  }

  // Check whether this new window is the most appropriate to focus.
  UpdateWindowFocus();
}

// Finds the top-most window, and if it doesn't have focus, then gives it focus.
void CastFocusClientAura::UpdateWindowFocus() {
  aura::Window* window = GetWindowToFocus();
  if (window == focused_window_) {
    return;
  }

  if (window) {
    aura::Window* top_level = GetZOrderWindow(window);
    DCHECK(top_level);
    DLOG(INFO) << "Switching focus to " << LOG_WINDOW_INFO(top_level, window);
  }

  aura::Window* unfocus_window = focused_window_;
  focused_window_ = window;

  for (aura::client::FocusChangeObserver& observer : focus_observers_) {
    observer.OnWindowFocused(focused_window_, unfocus_window);
    if (focused_window_ != window) {
      // The observer changed focused_window_.
      return;
    }
  }

  if (unfocus_window) {
    aura::client::FocusChangeObserver* focus_observer =
        aura::client::GetFocusChangeObserver(unfocus_window);
    if (focus_observer) {
      focus_observer->OnWindowFocused(focused_window_, unfocus_window);
      if (focused_window_ != window) {
        // The observer changed focused_window_.
        return;
      }
    }
  }
  if (focused_window_) {
    aura::client::FocusChangeObserver* focus_observer =
        aura::client::GetFocusChangeObserver(focused_window_);
    if (focus_observer) {
      focus_observer->OnWindowFocused(focused_window_, unfocus_window);
      if (focused_window_ != window) {
        // The observer changed focused_window_.
        return;
      }
    }
  }
}

// Returns the most appropriate window to have focus.
// A focusable window could be anywhere within its window hierarchy, and we
// choose based on the z-order of the top-level window in its hierarchy.
aura::Window* CastFocusClientAura::GetWindowToFocus() {
  aura::Window* next = nullptr;
  aura::Window* next_top_level = nullptr;
  for (aura::Window* window : focusable_windows_) {
    if (!window->CanFocus() || !window->IsVisible()) {
      continue;
    }

    // Compare z-order of top-level windows using the window IDs.
    aura::Window* top_level = GetZOrderWindow(window);
    DCHECK(top_level);
    if (!next || top_level->GetId() >= next_top_level->GetId()) {
      next = window;
      next_top_level = top_level;
    }
  }
  return next;
}

const aura::Window* CastFocusClientAura::GetZOrderWindow(
    const aura::Window* window) const {
  while (window->parent() && !window->parent()->IsRootWindow()) {
    window = window->parent();
  }
  return window;
}

void CastFocusClientAura::ResetFocusWithinActiveWindow(aura::Window* window) {
  // Sets focus to |window| if it's within the active window (a child of the
  // focused window).
  if (focused_window_ && focused_window_->Contains(window)) {
    FocusWindow(window);
  }
}

void CastFocusClientAura::AddObserver(wm::ActivationChangeObserver* observer) {}

void CastFocusClientAura::RemoveObserver(
    wm::ActivationChangeObserver* observer) {}

void CastFocusClientAura::ActivateWindow(aura::Window* window) {}

void CastFocusClientAura::DeactivateWindow(aura::Window* window) {}

const aura::Window* CastFocusClientAura::GetActiveWindow() const {
  return nullptr;
}

aura::Window* CastFocusClientAura::GetActivatableWindow(
    aura::Window* window) const {
  return window;
}

const aura::Window* CastFocusClientAura::GetToplevelWindow(
    const aura::Window* window) const {
  return GetZOrderWindow(window);
}

bool CastFocusClientAura::CanActivateWindow(const aura::Window* window) const {
  return true;
}

}  // namespace chromecast