/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsWindowTaskbarConcealer.h" #include "nsIWinTaskbar.h" #define NS_TASKBAR_CONTRACTID "@mozilla.org/windows-taskbar;1" #include "mozilla/Logging.h" #include "mozilla/StaticPrefs_widget.h" #include "mozilla/WindowsVersion.h" #include "WinUtils.h" using namespace mozilla; /** * TaskbarConcealerImpl * * Implement Windows-fullscreen marking. * * nsWindow::TaskbarConcealer implements logic determining _whether_ to tell * Windows that a given window is fullscreen. TaskbarConcealerImpl performs the * platform-specific work of actually communicating that fact to Windows. * * (This object is not persistent; it's constructed on the stack when needed.) */ struct TaskbarConcealerImpl { void MarkAsHidingTaskbar(HWND aWnd, bool aMark); private: nsCOMPtr mTaskbarInfo; }; /** * nsWindow::TaskbarConcealer * * Issue taskbar-hide requests to the OS as needed. */ /* Per MSDN [0], one should mark and unmark fullscreen windows via the ITaskbarList2::MarkFullscreenWindow method. Unfortunately, Windows pays less attention to this than one might prefer -- in particular, it typically fails to show the taskbar when switching focus from a window marked as fullscreen to one not thus marked. [1] Experimentation has (so far) suggested that its behavior is reasonable when switching between multiple monitors, or between a set of windows which are all from different processes [2]. This leaves us to handle the same-monitor, same- process case. Rather than do anything subtle here, we take the blanket approach of simply listening for every potentially-relevant state change, and then explicitly marking or unmarking every potentially-visible toplevel window. ---- [0] Relevant link: https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist2-markfullscreenwindow The "NonRudeHWND" property described therein doesn't help with anything in this comment, unfortunately. (See its use in MarkAsHidingTaskbar for more details.) [1] This is an oversimplification; Windows' actual behavior here is... complicated. See bug 1732517 comment 6 for some examples. [2] A comment in Chromium asserts that this is actually different threads. For us, of course, that makes no difference. https://github.com/chromium/chromium/blob/2b822268bd3/ui/views/win/hwnd_message_handler.cc#L1342 */ /************************************************************** * * SECTION: TaskbarConcealer utilities * **************************************************************/ static mozilla::LazyLogModule sTaskbarConcealerLog("TaskbarConcealer"); // Map of all relevant Gecko windows, along with the monitor on which each // window was last known to be located. /* static */ nsTHashMap nsWindow::TaskbarConcealer::sKnownWindows; // Preference for changes associated with bug 1732517. When false, revert to the // previous simple behavior of "Firefox fullscreen == Windows fullscreen". // // For simplicity-of-implementation's sake, changes to this pref require a // restart of Firefox to take effect. static bool UseAlternateFullscreenHeuristics() { static const bool val = StaticPrefs::widget_windows_alternate_fullscreen_heuristics(); return val; } // Returns Nothing if the window in question is irrelevant (for any reason), // or Some(the window's current state) otherwise. /* static */ Maybe nsWindow::TaskbarConcealer::GetWindowState(HWND aWnd) { // Classical Win32 visibility conditions. if (!::IsWindowVisible(aWnd)) { return Nothing(); } if (::IsIconic(aWnd)) { return Nothing(); } // Non-nsWindow windows associated with this thread may include file dialogs // and IME input popups. nsWindow* pWin = widget::WinUtils::GetNSWindowPtr(aWnd); if (!pWin) { return Nothing(); } // nsWindows of other window-classes include tooltips and drop-shadow-bearing // menus. if (pWin->mWindowType != WindowType::TopLevel) { return Nothing(); } // Cloaked windows are (presumably) on a different virtual desktop. // https://devblogs.microsoft.com/oldnewthing/20200302-00/?p=103507 if (pWin->mIsCloaked) { return Nothing(); } return Some( WindowState{::MonitorFromWindow(aWnd, MONITOR_DEFAULTTONULL), pWin->mFrameState->GetSizeMode() == nsSizeMode_Fullscreen}); } /************************************************************** * * SECTION: TaskbarConcealer::UpdateAllState * **************************************************************/ // Update all Windows-fullscreen-marking state and internal caches to represent // the current state of the system. /* static */ void nsWindow::TaskbarConcealer::UpdateAllState( HWND destroyedHwnd /* = nullptr */ ) { // sKnownWindows is otherwise-unprotected shared state MOZ_ASSERT(NS_IsMainThread(), "TaskbarConcealer can only be used from the main thread!"); if (MOZ_LOG_TEST(sTaskbarConcealerLog, LogLevel::Info)) { static size_t sLogCounter = 0; MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, ("Calling UpdateAllState() for the %zuth time", sLogCounter++)); MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, ("Last known state:")); if (sKnownWindows.IsEmpty()) { MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, (" none (no windows known)")); } else { for (const auto& entry : sKnownWindows) { MOZ_LOG( sTaskbarConcealerLog, LogLevel::Info, (" window %p was on monitor %p", entry.GetKey(), entry.GetData())); } } } // Array of all our potentially-relevant HWNDs, in Z-order (topmost first), // along with their associated relevant state. struct Item { HWND hwnd; HMONITOR monitor; bool isGkFullscreen; }; const nsTArray windows = [&] { nsTArray windows; // USE OF UNDOCUMENTED BEHAVIOR: The EnumWindows family of functions // enumerates windows in Z-order, topmost first. (This has been true since // at least Windows 2000, and possibly since Windows 3.0.) // // It's necessarily unreliable if windows are reordered while being // enumerated; but in that case we'll get a message informing us of that // fact, and can redo our state-calculations then. // // There exists no documented interface to acquire this information (other // than ::GetWindow(), which is racy). mozilla::EnumerateThreadWindows([&](HWND hwnd) { // Depending on details of window-destruction that probably shouldn't be // relied on, this HWND may or may not still be in the window list. // Pretend it's not. if (hwnd == destroyedHwnd) { return; } const auto maybeState = GetWindowState(hwnd); if (!maybeState) { return; } const WindowState& state = *maybeState; windows.AppendElement(Item{.hwnd = hwnd, .monitor = state.monitor, .isGkFullscreen = state.isGkFullscreen}); }); return windows; }(); // Relevant monitors are exactly those with relevant windows. const nsTHashSet relevantMonitors = [&]() { nsTHashSet relevantMonitors; for (const Item& item : windows) { relevantMonitors.Insert(item.monitor); } return relevantMonitors; }(); // Update the cached mapping from windows to monitors. (This is only used as // an optimization in TaskbarConcealer::OnWindowPosChanged().) sKnownWindows.Clear(); for (const Item& item : windows) { MOZ_LOG( sTaskbarConcealerLog, LogLevel::Debug, ("Found relevant window %p on monitor %p", item.hwnd, item.monitor)); sKnownWindows.InsertOrUpdate(item.hwnd, item.monitor); } // Auxiliary function. Does what it says on the tin. const auto FindUppermostWindowOn = [&windows](HMONITOR aMonitor) -> HWND { for (const Item& item : windows) { if (item.monitor == aMonitor) { MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, ("on monitor %p, uppermost relevant HWND is %p", aMonitor, item.hwnd)); return item.hwnd; } } // This should never happen, since we're drawing our monitor-set from the // set of relevant windows. MOZ_LOG(sTaskbarConcealerLog, LogLevel::Warning, ("on monitor %p, no relevant windows were found", aMonitor)); return nullptr; }; TaskbarConcealerImpl impl; // Mark all relevant windows as not hiding the taskbar, unless they're both // Gecko-fullscreen and the uppermost relevant window on their monitor. for (HMONITOR monitor : relevantMonitors) { const HWND topmost = FindUppermostWindowOn(monitor); for (const Item& item : windows) { if (item.monitor != monitor) continue; impl.MarkAsHidingTaskbar(item.hwnd, item.isGkFullscreen && item.hwnd == topmost); } } } // nsWindow::TaskbarConcealer::UpdateAllState() // Mark this window as requesting to occlude the taskbar. (The caller is // responsible for keeping any local state up-to-date.) void TaskbarConcealerImpl::MarkAsHidingTaskbar(HWND aWnd, bool aMark) { // USE OF UNDOCUMENTED BEHAVIOR: // // `MarkFullscreenWindow` is documented not to be sufficient. It will indeed // cause a window to be treated as fullscreen; but, in its absence, Windows // will also use explicitly undocumented heuristics to determine whether or // not to treat a given window as full-screen. // // In Windows 8.1 and later, these heuristics don't seem to apply to us. // However, in Windows 7, they do -- they determine that our fullscreen // windows are, indeed, fullscreen. (That this is technically correct is of // little importance, given that Windows then goes on to do the wrong thing // with that knowledge.) // // Fortunately, `MarkFullscreenWindow` does have a converse: the `NonRudeHWND` // window property. A window with this property set will not be treated as // fullscreen. // // === // // DIFFERENCE FROM DOCUMENTED BEHAVIOR: // // The documentation, as it was at the time of writing, is archived at: // https://web.archive.org/web/20211223073250/https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist2-markfullscreenwindow // // The most relevant paragraph follows: // // > **Since Windows 7**, call `SetProp(hwnd, L”NonRudeHWND”, // > reinterpret_cast(TRUE))` before showing a window to indicate to // > the Shell that the window should not be treated as full-screen. // // The key words in that paragraph are "before showing a window". On Windows 7 // this has no particular effect, but it completely changes the behavior on // Windows 8.1 and Windows 10 -- if `NonRudeHWND` is set on a window before it // is shown, that window will not be treated as fullscreen **even if the // property is later removed!** // // `NonRudeHWND` isn't actually documented to do anything at all if it's set // after the window has already been shown. That it seems to do exactly what // we need on Windows 7 -- prevent a window from being detected as fullscreen // while it's set, and only then -- is a stroke of fortune. static const bool kUseWin7MarkingHack = [&] { switch (StaticPrefs::widget_windows_fullscreen_marking_workaround()) { case -1: return false; case 1: return true; default: // The behavior on Windows 8 is not known. Hopefully there are no // side effects there. return !mozilla::IsWin8Point1OrLater(); } }(); if (kUseWin7MarkingHack) { constexpr static LPCWSTR kPropName = L"NonRudeHWND"; if (aMark) { ::RemovePropW(aWnd, kPropName); } else { ::SetPropW(aWnd, kPropName, reinterpret_cast(TRUE)); } } const char* const sMark = aMark ? "true" : "false"; if (!mTaskbarInfo) { mTaskbarInfo = do_GetService(NS_TASKBAR_CONTRACTID); if (!mTaskbarInfo) { MOZ_LOG( sTaskbarConcealerLog, LogLevel::Warning, ("could not acquire IWinTaskbar (aWnd %p, aMark %s)", aWnd, sMark)); return; } } MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, ("Calling PrepareFullScreen(%p, %s)", aWnd, sMark)); const nsresult hr = mTaskbarInfo->PrepareFullScreen(aWnd, aMark); if (FAILED(hr)) { MOZ_LOG(sTaskbarConcealerLog, LogLevel::Error, ("Call to PrepareFullScreen(%p, %s) failed with nsresult %x", aWnd, sMark, hr)); } }; /************************************************************** * * SECTION: TaskbarConcealer event callbacks * **************************************************************/ void nsWindow::TaskbarConcealer::OnWindowDestroyed(HWND aWnd) { if (!UseAlternateFullscreenHeuristics()) { return; } MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, ("==> OnWindowDestroyed() for HWND %p", aWnd)); UpdateAllState(aWnd); } void nsWindow::TaskbarConcealer::OnFocusAcquired(nsWindow* aWin) { if (!UseAlternateFullscreenHeuristics()) { return; } // Update state unconditionally. // // This is partially because focus-acquisition only updates the z-order, which // we don't cache and therefore can't notice changes to -- but also because // it's probably a good idea to give the user a natural way to refresh the // current fullscreen-marking state if it's somehow gone bad. MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, ("==> OnFocusAcquired() for HWND %p on HMONITOR %p", aWin->mWnd, ::MonitorFromWindow(aWin->mWnd, MONITOR_DEFAULTTONULL))); UpdateAllState(); } void nsWindow::TaskbarConcealer::OnFullscreenChanged(nsWindow* aWin, bool enteredFullscreen) { if (!UseAlternateFullscreenHeuristics()) { TaskbarConcealerImpl().MarkAsHidingTaskbar(aWin->mWnd, enteredFullscreen); return; } MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, ("==> OnFullscreenChanged() for HWND %p on HMONITOR %p", aWin->mWnd, ::MonitorFromWindow(aWin->mWnd, MONITOR_DEFAULTTONULL))); UpdateAllState(); } void nsWindow::TaskbarConcealer::OnWindowPosChanged(nsWindow* aWin) { if (!UseAlternateFullscreenHeuristics()) { return; } // Optimization: don't bother updating the state if the window hasn't moved // (including appearances and disappearances). const HWND myHwnd = aWin->mWnd; const HMONITOR oldMonitor = sKnownWindows.Get(myHwnd); // or nullptr const HMONITOR newMonitor = GetWindowState(myHwnd) .map([](auto state) { return state.monitor; }) .valueOr(nullptr); if (oldMonitor == newMonitor) { return; } MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, ("==> OnWindowPosChanged() for HWND %p (HMONITOR %p -> %p)", myHwnd, oldMonitor, newMonitor)); UpdateAllState(); } void nsWindow::TaskbarConcealer::OnCloakChanged() { if (!UseAlternateFullscreenHeuristics()) { return; } MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, ("==> OnCloakChanged()")); UpdateAllState(); }