зеркало из https://github.com/mozilla/gecko-dev.git
438 строки
15 KiB
C++
438 строки
15 KiB
C++
/* -*- 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();
|
|
}
|