diff options
Diffstat (limited to '')
-rw-r--r-- | widget/windows/nsWindowTaskbarConcealer.cpp | 437 |
1 files changed, 437 insertions, 0 deletions
diff --git a/widget/windows/nsWindowTaskbarConcealer.cpp b/widget/windows/nsWindowTaskbarConcealer.cpp new file mode 100644 index 0000000000..1863723cee --- /dev/null +++ b/widget/windows/nsWindowTaskbarConcealer.cpp @@ -0,0 +1,437 @@ +/* -*- 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<nsIWinTaskbar> 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<HWND, HMONITOR> 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::WindowState> +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<Item> windows = [&] { + nsTArray<Item> 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<HMONITOR> relevantMonitors = [&]() { + nsTHashSet<HMONITOR> 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<HANDLE>(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<HANDLE>(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(); +} |