| // Copyright 2018 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 "ui/aura/native_window_occlusion_tracker_win.h" |
| |
| #include <dwmapi.h> |
| #include <powersetting.h> |
| #include <memory> |
| #include <string> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/cxx20_erase.h" |
| #include "base/feature_list.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/strings/string_util_win.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/synchronization/waitable_event.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/threading/sequenced_task_runner_handle.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/win/scoped_gdi_object.h" |
| #include "base/win/windows_version.h" |
| #include "ui/aura/window_occlusion_tracker.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/gfx/geometry/skia_conversions.h" |
| #include "ui/gfx/win/hwnd_util.h" |
| |
| namespace aura { |
| |
| namespace { |
| |
| // ~16 ms = time between frames when frame rate is 60 FPS. |
| const base::TimeDelta kUpdateOcclusionDelay = base::Milliseconds(16); |
| |
| // This global variable can be accessed only on main thread. |
| NativeWindowOcclusionTrackerWin* g_tracker = nullptr; |
| |
| // The occluded region is calcuated in window pixel coordinates (aka screen |
| // coordinates). This maps it to DIPs (which the rest of the code needs) and |
| // converts to client coordinates (if necessary). |
| SkRegion AdjustForClientAndConvertToDips( |
| const SkRegion& region_pixels, |
| float scale_factor, |
| const gfx::Rect& window_tree_host_bounds_pixels) { |
| if (region_pixels.isEmpty()) |
| return region_pixels; |
| |
| // The region was calculated in window coordinates. If the WindowTreeHost is |
| // only using the client area, then the supplied region needs to be |
| // converted to client area and constrained to the client area. |
| SkRegion region(region_pixels); |
| region.op(gfx::RectToSkIRect(window_tree_host_bounds_pixels), |
| SkRegion::kIntersect_Op); |
| region.translate(window_tree_host_bounds_pixels.x(), |
| window_tree_host_bounds_pixels.y()); |
| if (region.isEmpty() || scale_factor == 1.0f) |
| return region; |
| |
| // Convert to dips. |
| SkRegion::Iterator iter(region); |
| std::vector<SkIRect> rects; |
| while (!iter.done()) { |
| const gfx::Rect rect_pixels = gfx::SkIRectToRect(iter.rect()); |
| rects.push_back(gfx::RectToSkIRect(gfx::ScaleToEnclosedRect( |
| rect_pixels, 1.0f / scale_factor, 1.0f / scale_factor))); |
| iter.next(); |
| } |
| SkRegion result; |
| result.setRects(rects.data(), rects.size()); |
| return result; |
| } |
| |
| } // namespace |
| |
| NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator* |
| NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator::instance_ = |
| nullptr; |
| |
| NativeWindowOcclusionTrackerWin* |
| NativeWindowOcclusionTrackerWin::GetOrCreateInstance() { |
| if (!g_tracker) |
| g_tracker = new NativeWindowOcclusionTrackerWin(); |
| |
| return g_tracker; |
| } |
| |
| void NativeWindowOcclusionTrackerWin::DeleteInstanceForTesting() { |
| delete g_tracker; |
| g_tracker = nullptr; |
| } |
| |
| void NativeWindowOcclusionTrackerWin::Enable(Window* window) { |
| DCHECK(window->IsRootWindow()); |
| if (window->HasObserver(this)) { |
| NOTREACHED() << "window shouldn't already be observing occlusion tracker"; |
| return; |
| } |
| // Add this as an observer so that we can be notified |
| // when it's no longer true that all windows are minimized, and when the |
| // window is destroyed. |
| HWND root_window_hwnd = window->GetHost()->GetAcceleratedWidget(); |
| window->AddObserver(this); |
| // Remember this mapping from hwnd to Window*. |
| hwnd_root_window_map_[root_window_hwnd] = window; |
| // Notify the occlusion thread of the new HWND to track. |
| update_occlusion_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &WindowOcclusionCalculator::EnableOcclusionTrackingForWindow, |
| base::Unretained(WindowOcclusionCalculator::GetInstance()), |
| root_window_hwnd)); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::Disable(Window* window) { |
| DCHECK(window->IsRootWindow()); |
| HWND root_window_hwnd = window->GetHost()->GetAcceleratedWidget(); |
| // Check that the root_window_hwnd doesn't get cleared before this is called. |
| DCHECK(root_window_hwnd); |
| hwnd_root_window_map_.erase(root_window_hwnd); |
| window->RemoveObserver(this); |
| update_occlusion_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &WindowOcclusionCalculator::DisableOcclusionTrackingForWindow, |
| base::Unretained(WindowOcclusionCalculator::GetInstance()), |
| root_window_hwnd)); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::OnWindowVisibilityChanged(Window* window, |
| bool visible) { |
| if (!window->IsRootWindow()) |
| return; |
| window->GetHost()->SetNativeWindowOcclusionState( |
| visible ? Window::OcclusionState::UNKNOWN |
| : Window::OcclusionState::HIDDEN, |
| {}); |
| update_occlusion_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&WindowOcclusionCalculator::HandleVisibilityChanged, |
| base::Unretained(WindowOcclusionCalculator::GetInstance()), |
| visible)); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::OnWindowDestroying(Window* window) { |
| Disable(window); |
| } |
| |
| NativeWindowOcclusionTrackerWin::NativeWindowOcclusionTrackerWin() |
| : // Use a COMSTATaskRunner so that registering and unregistering |
| // event hooks will happen on the same thread, as required by Windows, |
| // and the task runner will have a message loop to call |
| // EventHookCallback. |
| update_occlusion_task_runner_(base::ThreadPool::CreateCOMSTATaskRunner( |
| {base::MayBlock(), |
| // This may be needed to determine that a window is no longer |
| // occluded. |
| base::TaskPriority::USER_VISIBLE, |
| // Occlusion calculation doesn't need to happen on shutdown. |
| // event hooks should also be cleaned up by Windows. |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})), |
| session_change_observer_( |
| base::BindRepeating(&NativeWindowOcclusionTrackerWin::OnSessionChange, |
| base::Unretained(this))), |
| power_setting_change_listener_(this) { |
| WindowOcclusionCalculator::CreateInstance( |
| update_occlusion_task_runner_, base::SequencedTaskRunnerHandle::Get(), |
| base::BindRepeating( |
| &NativeWindowOcclusionTrackerWin::UpdateOcclusionState, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| NativeWindowOcclusionTrackerWin::~NativeWindowOcclusionTrackerWin() { |
| // This code is intended to be used in tests and shouldn't be reached in |
| // production. |
| |
| // The occlusion tracker should be destroyed after all windows; window |
| // destructors should call Disable() and thus remove them from the map, so by |
| // the time we reach here the map should be empty. (Proceeding with a |
| // non-empty map would result in CheckedObserver failure since any remaining |
| // windows still have the tracker as a registered observer.) |
| DCHECK(hwnd_root_window_map_.empty()) |
| << "Occlusion tracker torn down while a Window still exists"; |
| |
| // |occlusion_calculator_| must be deleted on its sequence because it needs |
| // to unregister event hooks on COMSTA thread. This blocks the main thread. |
| base::WaitableEvent done_event; |
| update_occlusion_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&WindowOcclusionCalculator::DeleteInstanceForTesting, |
| &done_event)); |
| done_event.Wait(); |
| } |
| |
| // static |
| bool NativeWindowOcclusionTrackerWin::IsWindowVisibleAndFullyOpaque( |
| HWND hwnd, |
| gfx::Rect* window_rect) { |
| // Filter out windows that are not "visible", IsWindowVisible(). |
| if (!IsWindow(hwnd) || !IsWindowVisible(hwnd)) |
| return false; |
| |
| // Filter out minimized windows. |
| if (IsIconic(hwnd)) |
| return false; |
| |
| LONG ex_styles = ::GetWindowLong(hwnd, GWL_EXSTYLE); |
| // Filter out "transparent" windows, windows where the mouse clicks fall |
| // through them. |
| if (ex_styles & WS_EX_TRANSPARENT) |
| return false; |
| |
| // Filter out "tool windows", which are floating windows that do not appear on |
| // the taskbar or ALT-TAB. Floating windows can have larger window rectangles |
| // than what is visible to the user, so by filtering them out we will avoid |
| // incorrectly marking native windows as occluded. We do not filter out the |
| // Windows Taskbar. |
| if (ex_styles & WS_EX_TOOLWINDOW) { |
| if (gfx::GetClassName(hwnd) != L"Shell_TrayWnd") |
| return false; |
| } |
| |
| // Filter out layered windows that are not opaque or that set a transparency |
| // colorkey. |
| if (ex_styles & WS_EX_LAYERED) { |
| BYTE alpha; |
| DWORD flags; |
| |
| // GetLayeredWindowAttributes only works if the application has |
| // previously called SetLayeredWindowAttributes on the window. |
| // The function will fail if the layered window was setup with |
| // UpdateLayeredWindow. Treat this failure as the window being transparent. |
| // See Remarks section of |
| // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getlayeredwindowattributes |
| if (!GetLayeredWindowAttributes(hwnd, nullptr, &alpha, &flags)) |
| return false; |
| |
| if (flags & LWA_ALPHA && alpha < 255) |
| return false; |
| if (flags & LWA_COLORKEY) |
| return false; |
| } |
| |
| // Filter out windows that do not have a simple rectangular region. |
| base::win::ScopedRegion region(CreateRectRgn(0, 0, 0, 0)); |
| if (GetWindowRgn(hwnd, region.get()) == COMPLEXREGION) |
| return false; |
| |
| // Windows 10 has cloaked windows, windows with WS_VISIBLE attribute but |
| // not displayed. explorer.exe, in particular has one that's the |
| // size of the desktop. It's usually behind Chrome windows in the z-order, |
| // but using a remote desktop can move it up in the z-order. So, ignore them. |
| DWORD reason; |
| if (SUCCEEDED(DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &reason, |
| sizeof(reason))) && |
| reason != 0) { |
| return false; |
| } |
| |
| RECT win_rect; |
| // Filter out windows that take up zero area. The call to GetWindowRect is one |
| // of the most expensive parts of this function, so it is last. |
| if (!GetWindowRect(hwnd, &win_rect)) |
| return false; |
| if (IsRectEmpty(&win_rect)) |
| return false; |
| |
| // Ignore popup windows since they're transient unless it is a Chrome Widget |
| // Window or the Windows Taskbar |
| if (::GetWindowLong(hwnd, GWL_STYLE) & WS_POPUP) { |
| std::wstring hwnd_class_name = gfx::GetClassName(hwnd); |
| if (!base::StartsWith(hwnd_class_name, L"Chrome_WidgetWin_") && |
| hwnd_class_name != L"Shell_TrayWnd") { |
| return false; |
| } |
| } |
| |
| *window_rect = gfx::Rect(win_rect); |
| |
| WINDOWPLACEMENT window_placement = {0}; |
| window_placement.length = sizeof(WINDOWPLACEMENT); |
| ::GetWindowPlacement(hwnd, &window_placement); |
| if (window_placement.showCmd == SW_MAXIMIZE) { |
| // If the window is maximized the window border extends beyond the visible |
| // region of the screen. Adjust the maximized window rect to fit the |
| // screen dimensions to ensure that fullscreen windows, which do not extend |
| // beyond the screen boundaries since they typically have no borders, will |
| // occlude maximized windows underneath them. |
| HMONITOR hmon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); |
| if (hmon) { |
| MONITORINFO mi; |
| mi.cbSize = sizeof(mi); |
| if (GetMonitorInfo(hmon, &mi)) { |
| (*window_rect).AdjustToFit(gfx::Rect(mi.rcWork)); |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| void NativeWindowOcclusionTrackerWin::UpdateOcclusionState( |
| const HwndToRootOcclusionStateMap& root_window_hwnds_occlusion_state, |
| bool show_all_windows) { |
| // Pause occlusion until we've updated all root windows, to avoid O(n^3) |
| // calls to recompute occlusion in WindowOcclusionTracker. |
| WindowOcclusionTracker::ScopedPause pause_occlusion_tracking; |
| num_visible_root_windows_ = 0; |
| for (const auto& root_window_pair : root_window_hwnds_occlusion_state) { |
| auto it = hwnd_root_window_map_.find(root_window_pair.first); |
| // The window was destroyed while processing occlusion. |
| if (it == hwnd_root_window_map_.end()) |
| continue; |
| // Check Window::IsVisible here, on the UI thread, because it can't be |
| // checked on the occlusion calculation thread. Do this first before |
| // checking screen_locked_ or display_on_ so that hidden windows remain |
| // hidden. |
| if (!it->second->IsVisible()) { |
| it->second->GetHost()->SetNativeWindowOcclusionState( |
| Window::OcclusionState::HIDDEN, {}); |
| continue; |
| } |
| Window::OcclusionState occl_state = root_window_pair.second.occlusion_state; |
| SkRegion occluded_region; |
| // If the screen is locked or off, ignore occlusion state results and |
| // mark the window as occluded. |
| if (screen_locked_ || !display_on_) { |
| occl_state = Window::OcclusionState::OCCLUDED; |
| } else if (show_all_windows) { |
| occl_state = Window::OcclusionState::VISIBLE; |
| } else if (occl_state == Window::OcclusionState::VISIBLE) { |
| occluded_region = AdjustForClientAndConvertToDips( |
| root_window_pair.second.occluded_region_pixels, |
| it->second->GetHost()->device_scale_factor(), |
| it->second->GetHost() |
| ->GetBoundsInAcceleratedWidgetPixelCoordinates()); |
| } |
| |
| it->second->GetHost()->SetNativeWindowOcclusionState(occl_state, |
| occluded_region); |
| num_visible_root_windows_++; |
| } |
| } |
| |
| void NativeWindowOcclusionTrackerWin::OnSessionChange( |
| WPARAM status_code, |
| const bool* is_current_session) { |
| if (is_current_session && !*is_current_session) |
| return; |
| if (status_code == WTS_SESSION_UNLOCK) { |
| screen_locked_ = false; |
| // We may not get a foreground event when unlocking the device so |
| // kick off occlusion recalculation now. |
| update_occlusion_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &WindowOcclusionCalculator::ForceRecalculation, |
| base::Unretained(WindowOcclusionCalculator::GetInstance()))); |
| } else if (status_code == WTS_SESSION_LOCK && is_current_session) { |
| screen_locked_ = true; |
| MarkNonIconicWindowsOccluded(); |
| } |
| } |
| |
| void NativeWindowOcclusionTrackerWin::OnDisplayStateChanged(bool display_on) { |
| static bool screen_power_listener_enabled = base::FeatureList::IsEnabled( |
| features::kScreenPowerListenerForNativeWinOcclusion); |
| if (!screen_power_listener_enabled) |
| return; |
| |
| if (display_on == display_on_) |
| return; |
| |
| display_on_ = display_on; |
| if (display_on_) { |
| // Notify the window occlusion calculator of the display turning on |
| // to chedule an occlusion calculation. This must be run on the |
| // WindowOcclusionCalculator thread. |
| update_occlusion_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &WindowOcclusionCalculator::ForceRecalculation, |
| base::Unretained(WindowOcclusionCalculator::GetInstance()))); |
| } else { |
| MarkNonIconicWindowsOccluded(); |
| } |
| } |
| |
| void NativeWindowOcclusionTrackerWin::OnResume() { |
| // Notify the window occlusion calculator of the device waking. |
| // This must be run on the WindowOcclusionCalculator thread. |
| update_occlusion_task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&WindowOcclusionCalculator::HandleResumeSuspend, |
| base::Unretained( |
| WindowOcclusionCalculator::GetInstance()))); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::OnSuspend() { |
| // Notify the window occlusion calculator of the device going to sleep. |
| // This must be run on the WindowOcclusionCalculator thread. |
| update_occlusion_task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&WindowOcclusionCalculator::HandleResumeSuspend, |
| base::Unretained( |
| WindowOcclusionCalculator::GetInstance()))); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::MarkNonIconicWindowsOccluded() { |
| // Set all visible root windows as occluded. If not visible, |
| // set them as hidden. |
| for (const auto& root_window_hwnd_pair : hwnd_root_window_map_) { |
| root_window_hwnd_pair.second->GetHost()->SetNativeWindowOcclusionState( |
| IsIconic(root_window_hwnd_pair.first) |
| ? Window::OcclusionState::HIDDEN |
| : Window::OcclusionState::OCCLUDED, |
| {}); |
| } |
| } |
| |
| NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| WindowOcclusionCalculator( |
| scoped_refptr<base::SequencedTaskRunner> task_runner, |
| scoped_refptr<base::SequencedTaskRunner> ui_thread_task_runner, |
| UpdateOcclusionStateCallback update_occlusion_state_callback) |
| : task_runner_(task_runner), |
| ui_thread_task_runner_(ui_thread_task_runner), |
| calculate_occluded_region_(base::FeatureList::IsEnabled( |
| features::kApplyNativeOccludedRegionToWindowTracker)), |
| update_occlusion_state_callback_(update_occlusion_state_callback) { |
| if (base::win::GetVersion() >= base::win::Version::WIN10) { |
| ::CoCreateInstance(__uuidof(VirtualDesktopManager), nullptr, CLSCTX_ALL, |
| IID_PPV_ARGS(&virtual_desktop_manager_)); |
| } |
| DETACH_FROM_SEQUENCE(sequence_checker_); |
| } |
| |
| NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| ~WindowOcclusionCalculator() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| UnregisterEventHooks(); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator::CreateInstance( |
| scoped_refptr<base::SequencedTaskRunner> task_runner, |
| scoped_refptr<base::SequencedTaskRunner> ui_thread_task_runner, |
| UpdateOcclusionStateCallback update_occlusion_state_callback) { |
| DCHECK(!instance_); |
| instance_ = new WindowOcclusionCalculator(task_runner, ui_thread_task_runner, |
| update_occlusion_state_callback); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| DeleteInstanceForTesting(base::WaitableEvent* done_event) { |
| DCHECK(instance_); |
| delete instance_; |
| instance_ = nullptr; |
| done_event->Signal(); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| EnableOcclusionTrackingForWindow(HWND hwnd) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| root_window_hwnds_occlusion_state_[hwnd] = {}; |
| if (global_event_hooks_.empty()) |
| RegisterEventHooks(); |
| |
| // Schedule an occlusion calculation so that the newly tracked window does |
| // not have a stale occlusion status. |
| ScheduleOcclusionCalculationIfNeeded(); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| DisableOcclusionTrackingForWindow(HWND hwnd) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| root_window_hwnds_occlusion_state_.erase(hwnd); |
| if (moving_window_ == hwnd) |
| moving_window_ = 0; |
| if (root_window_hwnds_occlusion_state_.empty()) { |
| UnregisterEventHooks(); |
| if (occlusion_update_timer_.IsRunning()) |
| occlusion_update_timer_.Stop(); |
| } |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| HandleVisibilityChanged(bool visible) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // May have gone from having no visible windows to having one, in |
| // which case we need to register event hooks, and make sure that an |
| // occlusion calculation is scheduled. |
| if (visible) { |
| MaybeRegisterEventHooks(); |
| ScheduleOcclusionCalculationIfNeeded(); |
| } |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| ForceRecalculation() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| MaybeRegisterEventHooks(); |
| ScheduleOcclusionCalculationIfNeeded(); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| HandleResumeSuspend() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // Timers are unreliable when the device is going to sleep or resuming. |
| // Stop the timer if it is currently running. |
| if (occlusion_update_timer_.IsRunning()) |
| occlusion_update_timer_.Stop(); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| MaybeRegisterEventHooks() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (global_event_hooks_.empty()) |
| RegisterEventHooks(); |
| } |
| |
| // static |
| void CALLBACK |
| NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator::EventHookCallback( |
| HWINEVENTHOOK hWinEventHook, |
| DWORD event, |
| HWND hwnd, |
| LONG idObject, |
| LONG idChild, |
| DWORD dwEventThread, |
| DWORD dwmsEventTime) { |
| if (instance_) |
| instance_->ProcessEventHookCallback(event, hwnd, idObject, idChild); |
| } |
| |
| // static |
| BOOL CALLBACK NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| ComputeNativeWindowOcclusionStatusCallback(HWND hwnd, LPARAM lParam) { |
| if (instance_) { |
| return instance_->ProcessComputeNativeWindowOcclusionStatusCallback( |
| hwnd, reinterpret_cast<base::flat_set<DWORD>*>(lParam)); |
| } |
| return FALSE; |
| } |
| |
| // static |
| BOOL CALLBACK NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| UpdateVisibleWindowProcessIdsCallback(HWND hwnd, LPARAM lParam) { |
| if (instance_) { |
| instance_->ProcessUpdateVisibleWindowProcessIdsCallback(hwnd); |
| return TRUE; |
| } |
| return FALSE; |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| UpdateVisibleWindowProcessIds() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| pids_for_location_change_hook_.clear(); |
| EnumWindows(&UpdateVisibleWindowProcessIdsCallback, 0); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| ComputeNativeWindowOcclusionStatus() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (root_window_hwnds_occlusion_state_.empty()) |
| return; |
| // Set up initial conditions for occlusion calculation. |
| bool should_unregister_event_hooks = true; |
| |
| // Compute the SkRegion for the screen. |
| int screen_left = GetSystemMetrics(SM_XVIRTUALSCREEN); |
| int screen_top = GetSystemMetrics(SM_YVIRTUALSCREEN); |
| SkRegion screen_region = SkRegion( |
| SkIRect::MakeLTRB(screen_left, screen_top, |
| screen_left + GetSystemMetrics(SM_CXVIRTUALSCREEN), |
| screen_top + GetSystemMetrics(SM_CYVIRTUALSCREEN))); |
| num_root_windows_with_unknown_occlusion_state_ = 0; |
| |
| for (auto& root_window_pair : root_window_hwnds_occlusion_state_) { |
| HWND hwnd = root_window_pair.first; |
| |
| // Reset RootOcclusionState to a clean state. |
| root_window_pair.second = {}; |
| // IsIconic() checks for a minimized window. Immediately set the state of |
| // minimized windows to HIDDEN. |
| if (IsIconic(hwnd)) { |
| root_window_pair.second.occlusion_state = Window::OcclusionState::HIDDEN; |
| } else if (IsWindowOnCurrentVirtualDesktop(hwnd) == false) { |
| // If window is not on the current virtual desktop, immediately |
| // set the state of the window to OCCLUDED. |
| root_window_pair.second.occlusion_state = |
| Window::OcclusionState::OCCLUDED; |
| // Don't unregister event hooks when not on current desktop. There's no |
| // notification when that changes, so we can't reregister event hooks. |
| should_unregister_event_hooks = false; |
| } else { |
| root_window_pair.second.occlusion_state = Window::OcclusionState::UNKNOWN; |
| should_unregister_event_hooks = false; |
| num_root_windows_with_unknown_occlusion_state_++; |
| } |
| } |
| // Unregister event hooks if all native windows are minimized. |
| if (should_unregister_event_hooks) { |
| UnregisterEventHooks(); |
| } else { |
| base::flat_set<DWORD> current_pids_with_visible_windows; |
| unoccluded_desktop_region_ = screen_region; |
| // Calculate unoccluded region if there is a non-minimized native window. |
| // Also compute |current_pids_with_visible_windows| as we enumerate |
| // the windows. |
| EnumWindows(&ComputeNativeWindowOcclusionStatusCallback, |
| reinterpret_cast<LPARAM>(¤t_pids_with_visible_windows)); |
| // Check if |pids_for_location_change_hook_| has any pids of processes |
| // currently without visible windows. If so, unhook the win event, |
| // remove the pid from |pids_for_location_change_hook_| and remove |
| // the corresponding event hook from |process_event_hooks_|. |
| base::flat_set<DWORD> pids_to_remove; |
| for (auto loc_change_pid : pids_for_location_change_hook_) { |
| if (current_pids_with_visible_windows.find(loc_change_pid) == |
| current_pids_with_visible_windows.end()) { |
| // Remove the event hook from our map, and unregister the event hook. |
| // It's possible the eventhook will no longer be valid, but if we don't |
| // unregister the event hook, a process that toggles between having |
| // visible windows and not having visible windows could cause duplicate |
| // event hooks to get registered for the process. |
| UnhookWinEvent(process_event_hooks_[loc_change_pid]); |
| process_event_hooks_.erase(loc_change_pid); |
| pids_to_remove.insert(loc_change_pid); |
| } |
| } |
| if (!pids_to_remove.empty()) { |
| // EraseIf is O(n) so erase pids not found in one fell swoop. |
| base::EraseIf(pids_for_location_change_hook_, |
| [&pids_to_remove](DWORD pid) { |
| return pids_to_remove.find(pid) != pids_to_remove.end(); |
| }); |
| } |
| } |
| // Post a task to the browser ui thread to update the window occlusion state |
| // on the root windows. |
| ui_thread_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(update_occlusion_state_callback_, |
| root_window_hwnds_occlusion_state_, showing_thumbnails_)); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| ScheduleOcclusionCalculationIfNeeded() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!occlusion_update_timer_.IsRunning()) { |
| occlusion_update_timer_.Start( |
| FROM_HERE, kUpdateOcclusionDelay, this, |
| &WindowOcclusionCalculator::ComputeNativeWindowOcclusionStatus); |
| } |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| RegisterGlobalEventHook(UINT event_min, UINT event_max) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| HWINEVENTHOOK event_hook = |
| SetWinEventHook(event_min, event_max, nullptr, &EventHookCallback, 0, 0, |
| WINEVENT_OUTOFCONTEXT); |
| |
| global_event_hooks_.push_back(event_hook); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| RegisterEventHookForProcess(DWORD pid) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| pids_for_location_change_hook_.insert(pid); |
| process_event_hooks_[pid] = SetWinEventHook( |
| EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_LOCATIONCHANGE, nullptr, |
| &EventHookCallback, pid, 0, WINEVENT_OUTOFCONTEXT); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| RegisterEventHooks() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(global_event_hooks_.empty()); |
| |
| // This helps with the case where an extension tried to foreground a window, |
| // but Windows prevented it, and the user clicks on the task bar to bring the |
| // window forward. See https://crbug.com/1137982. This is the only event I |
| // could find that always gets sent in this scenario, and isn't too common. |
| // TODO(crbug.com/1297684): See if we can make handling this event cheaper, |
| // since we typically don't need it, e.g., by using a longer timer interval |
| // for starting the occlusion calculation for this event, to batch subsequent |
| // events. |
| RegisterGlobalEventHook(EVENT_SYSTEM_CAPTUREEND, EVENT_SYSTEM_CAPTUREEND); |
| |
| // Detects native window move (drag) and resizing events. |
| RegisterGlobalEventHook(EVENT_SYSTEM_MOVESIZESTART, EVENT_SYSTEM_MOVESIZEEND); |
| |
| // Detects native window minimize and restore from taskbar events. |
| RegisterGlobalEventHook(EVENT_SYSTEM_MINIMIZESTART, EVENT_SYSTEM_MINIMIZEEND); |
| |
| // Detects foreground window changing. |
| RegisterGlobalEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND); |
| |
| // Detects objects getting shown and hidden. Used to know when the task bar |
| // and alt tab are showing preview windows so we can unocclude Chrome windows. |
| RegisterGlobalEventHook(EVENT_OBJECT_SHOW, EVENT_OBJECT_HIDE); |
| |
| // Detects object state changes, e.g., enable/disable state, native window |
| // maximize and native window restore events. |
| RegisterGlobalEventHook(EVENT_OBJECT_STATECHANGE, EVENT_OBJECT_STATECHANGE); |
| |
| // Cloaking and uncloaking of windows should trigger an occlusion calculation. |
| // In particular, switching virtual desktops seems to generate these events. |
| RegisterGlobalEventHook(EVENT_OBJECT_CLOAKED, EVENT_OBJECT_UNCLOAKED); |
| |
| // Determine which subset of processes to set EVENT_OBJECT_LOCATIONCHANGE on |
| // because otherwise event throughput is very high, as it generates events |
| // for location changes of all objects, including the mouse moving on top of a |
| // window. |
| UpdateVisibleWindowProcessIds(); |
| for (DWORD pid : pids_for_location_change_hook_) |
| RegisterEventHookForProcess(pid); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| UnregisterEventHooks() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| window_is_moving_ = false; |
| for (HWINEVENTHOOK event_hook : global_event_hooks_) |
| UnhookWinEvent(event_hook); |
| global_event_hooks_.clear(); |
| |
| for (DWORD pid : pids_for_location_change_hook_) |
| UnhookWinEvent(process_event_hooks_[pid]); |
| process_event_hooks_.clear(); |
| |
| pids_for_location_change_hook_.clear(); |
| } |
| |
| bool NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| ProcessComputeNativeWindowOcclusionStatusCallback( |
| HWND hwnd, |
| base::flat_set<DWORD>* current_pids_with_visible_windows) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| gfx::Rect window_rect; |
| const bool window_is_occluding = |
| WindowCanOccludeOtherWindowsOnCurrentVirtualDesktop(hwnd, &window_rect); |
| if (window_is_occluding) { |
| // Hook this window's process with EVENT_OBJECT_LOCATION_CHANGE, if we are |
| // not already doing so. |
| DWORD pid; |
| GetWindowThreadProcessId(hwnd, &pid); |
| current_pids_with_visible_windows->insert(pid); |
| if (!base::Contains(process_event_hooks_, pid)) |
| RegisterEventHookForProcess(pid); |
| } |
| |
| // Ignore moving windows when deciding if windows under it are occluded. |
| if (hwnd == moving_window_) |
| return true; |
| |
| // If no more root windows to consider, return true so we can continue |
| // looking for windows we haven't hooked. |
| if (num_root_windows_with_unknown_occlusion_state_ == 0) |
| return true; |
| |
| auto it = root_window_hwnds_occlusion_state_.find(hwnd); |
| |
| // Check if |hwnd| is a root window; if so, we're done figuring out |
| // if it's occluded because we've seen all the windows "over" it. |
| if (it == root_window_hwnds_occlusion_state_.end() || |
| it->second.occlusion_state != Window::OcclusionState::UNKNOWN) { |
| if (window_is_occluding) { |
| unoccluded_desktop_region_.op(gfx::RectToSkIRect(window_rect), |
| SkRegion::kDifference_Op); |
| } |
| return true; |
| } |
| |
| num_root_windows_with_unknown_occlusion_state_--; |
| |
| SkRegion occluded_window_region = unoccluded_desktop_region_; |
| SkRegion curr_unoccluded_destkop = unoccluded_desktop_region_; |
| if (window_is_occluding) { |
| unoccluded_desktop_region_.op(gfx::RectToSkIRect(window_rect), |
| SkRegion::kDifference_Op); |
| } |
| |
| // On Win7, default theme makes root windows have complex regions by |
| // default. But we can still check if their bounding rect is occluded. |
| if (!window_is_occluding) { |
| RECT rect; |
| if (::GetWindowRect(hwnd, &rect) != 0) { |
| SkRegion window_region( |
| SkIRect::MakeLTRB(rect.left, rect.top, rect.right, rect.bottom)); |
| |
| window_rect = gfx::Rect(rect); |
| curr_unoccluded_destkop.op(window_region, SkRegion::kDifference_Op); |
| } |
| } |
| if (unoccluded_desktop_region_ == curr_unoccluded_destkop) { |
| it->second.occlusion_state = Window::OcclusionState::OCCLUDED; |
| return true; |
| } |
| it->second.occlusion_state = Window::OcclusionState::VISIBLE; |
| if (!calculate_occluded_region_ || window_rect.IsEmpty()) |
| return true; |
| |
| occluded_window_region.op(gfx::RectToSkIRect(window_rect), |
| SkRegion::kIntersect_Op); |
| if (occluded_window_region.isEmpty()) |
| return true; |
| |
| occluded_window_region.op(gfx::RectToSkIRect(window_rect), |
| SkRegion::kReverseDifference_Op); |
| occluded_window_region.translate(-window_rect.x(), -window_rect.y()); |
| it->second.occluded_region_pixels.swap(occluded_window_region); |
| return true; |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| ProcessEventHookCallback(DWORD event, |
| HWND hwnd, |
| LONG id_object, |
| LONG id_child) { |
| // Can't do DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_) here. See |
| // comment before call to PostTask below as to why. |
| |
| // No need to calculate occlusion if a zero HWND generated the event. This |
| // happens if there is no window associated with the event, e.g., mouse move |
| // events. |
| if (!hwnd) |
| return; |
| |
| // We only care about events for window objects. In particular, we don't care |
| // about OBJID_CARET, which is spammy. |
| if (id_object != OBJID_WINDOW) |
| return; |
| |
| // We generally ignore events for popup windows, except for when the taskbar |
| // is hidden or when the popup is a Chrome Widget or Windows Taskbar, in |
| // which case we recalculate occlusion. |
| bool calculate_occlusion = true; |
| if (::GetWindowLong(hwnd, GWL_STYLE) & WS_POPUP) { |
| std::wstring hwnd_class_name = gfx::GetClassName(hwnd); |
| calculate_occlusion = |
| base::StartsWith(hwnd_class_name, L"Chrome_WidgetWin_") || |
| hwnd_class_name == L"Shell_TrayWnd"; |
| } |
| |
| // Detect if either the alt tab view or the task list thumbnail is being |
| // shown. If so, mark all non-hidden windows as occluded, and remember that |
| // we're in the showing_thumbnails state. This lasts until we get told that |
| // either the alt tab view or task list thumbnail are hidden. |
| if (event == EVENT_OBJECT_SHOW) { |
| // Avoid getting the hwnd's class name, and recomputing occlusion, if not |
| // needed. |
| if (showing_thumbnails_) |
| return; |
| std::string hwnd_class_name = base::WideToUTF8(gfx::GetClassName(hwnd)); |
| if ((hwnd_class_name == "MultitaskingViewFrame" || |
| hwnd_class_name == "TaskListThumbnailWnd")) { |
| showing_thumbnails_ = true; |
| ui_thread_task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(update_occlusion_state_callback_, |
| root_window_hwnds_occlusion_state_, |
| showing_thumbnails_)); |
| } |
| return; |
| } else if (event == EVENT_OBJECT_HIDE) { |
| // Avoid getting the hwnd's class name, and recomputing occlusion, if not |
| // needed. |
| if (!showing_thumbnails_) |
| return; |
| std::string hwnd_class_name = base::WideToUTF8(gfx::GetClassName(hwnd)); |
| if (hwnd_class_name == "MultitaskingViewFrame" || |
| hwnd_class_name == "TaskListThumbnailWnd") { |
| showing_thumbnails_ = false; |
| // Let occlusion calculation fix occlusion state, even though hwnd might |
| // be a popup window. |
| calculate_occlusion = true; |
| } else { |
| return; |
| } |
| } |
| // Don't continually calculate occlusion while a window is moving (unless it's |
| // a root window), but instead once at the beginning and once at the end. |
| // Remember the window being moved so if it's a root window, we can ignore |
| // it when deciding if windows under it are occluded. |
| else if (event == EVENT_SYSTEM_MOVESIZESTART) { |
| moving_window_ = hwnd; |
| } else if (event == EVENT_SYSTEM_MOVESIZEEND) { |
| moving_window_ = 0; |
| } else if (moving_window_ != 0) { |
| if (event == EVENT_OBJECT_LOCATIONCHANGE || |
| event == EVENT_OBJECT_STATECHANGE) { |
| // Ignore move events if it's not a root window that's being moved. If it |
| // is a root window, we want to calculate occlusion to support tab |
| // dragging to windows that were occluded when the drag was started but |
| // are no longer occluded. |
| if (root_window_hwnds_occlusion_state_.find(hwnd) == |
| root_window_hwnds_occlusion_state_.end()) { |
| return; |
| } |
| } else { |
| // If we get an event that isn't a location/state change, then we probably |
| // missed the movesizeend notification, or got events out of order. In |
| // that case, we want to go back to normal occlusion calculation. |
| moving_window_ = 0; |
| } |
| } |
| |
| if (!calculate_occlusion) |
| return; |
| |
| // ProcessEventHookCallback is called from the task_runner's PeekMessage |
| // call, on the task runner's thread, but before the task_tracker thread sets |
| // up the thread sequence. In order to prevent DCHECK failures with the |
| // |occlusion_update_timer_, we need to call |
| // ScheduleOcclusionCalculationIfNeeded from a task. |
| // See WorkerThreadCOMDelegate::GetWorkFromWindowsMessageQueue(). |
| task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &WindowOcclusionCalculator::ScheduleOcclusionCalculationIfNeeded, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| ProcessUpdateVisibleWindowProcessIdsCallback(HWND hwnd) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| gfx::Rect window_rect; |
| if (WindowCanOccludeOtherWindowsOnCurrentVirtualDesktop(hwnd, &window_rect)) { |
| DWORD pid; |
| GetWindowThreadProcessId(hwnd, &pid); |
| pids_for_location_change_hook_.insert(pid); |
| } |
| } |
| |
| bool NativeWindowOcclusionTrackerWin::WindowOcclusionCalculator:: |
| WindowCanOccludeOtherWindowsOnCurrentVirtualDesktop( |
| HWND hwnd, |
| gfx::Rect* window_rect) { |
| return IsWindowVisibleAndFullyOpaque(hwnd, window_rect) && |
| (IsWindowOnCurrentVirtualDesktop(hwnd) == true); |
| } |
| |
| absl::optional<bool> NativeWindowOcclusionTrackerWin:: |
| WindowOcclusionCalculator::IsWindowOnCurrentVirtualDesktop(HWND hwnd) { |
| if (!virtual_desktop_manager_) |
| return true; |
| |
| return gfx::IsWindowOnCurrentVirtualDesktop(hwnd, virtual_desktop_manager_); |
| } |
| |
| } // namespace aura |