Bug 1819421 - [1/2] create parallel check for Win11 tablet mode r=win-reviewers,Gijs,gsvelto,gstoll

Win11's tablet mode is different from Win10's tablet mode in ways both
subtle and gross. Avoid unifying them implicitly, and make it clear that
they should not be unified _explicitly_ without testing.

Unfortunately, Windows 11's API for checking whether the device is in
tablet mode is broken -- it requires one to know whether the device is a
tablet to interpret the return value, but there is no documented API
providing that information. We use a heuristic partly borrowed from
Chromium and partly based on independent investigation.

As all of the code using WindowsUIUtils effectively only checked for
Win10's tablet mode, this commit technically has no functional
changes.

Differential Revision: https://phabricator.services.mozilla.com/D224244
This commit is contained in:
Ray Kraesig 2024-11-04 13:56:20 +00:00
parent 3995ae42c9
commit 38458eed44
11 changed files with 324 additions and 27 deletions

View File

@ -4656,7 +4656,7 @@ function updateToggleControlLabel(control) {
var TabletModeUpdater = {
init() {
if (AppConstants.platform == "win") {
this.update(WindowsUIUtils.inTabletMode);
this.update(WindowsUIUtils.inWin10TabletMode);
Services.obs.addObserver(this, "tablet-mode-change");
}
},
@ -4722,7 +4722,7 @@ var gUIDensity = {
// Automatically override the uidensity to touch in Windows tablet mode.
if (
AppConstants.platform == "win" &&
WindowsUIUtils.inTabletMode &&
WindowsUIUtils.inWin10TabletMode &&
Services.prefs.getBoolPref(this.autoTouchModePref)
) {
return { mode: this.MODE_TOUCH, overridden: true };

View File

@ -1583,7 +1583,7 @@ nsDefaultCommandLineHandler.prototype = {
if (
AppConstants.platform == "win" &&
cmdLine.state != Ci.nsICommandLine.STATE_INITIAL_LAUNCH &&
lazy.WindowsUIUtils.inTabletMode
lazy.WindowsUIUtils.inWin10TabletMode
) {
// In windows 10 tablet mode, do not create a new window, but reuse the existing one.
let win = lazy.BrowserWindowTracker.getTopWindow();

View File

@ -17616,6 +17616,17 @@
value: true
mirror: always
# Whether this device is capable of entering tablet mode. (Win11+ only.)
#
# Valid values:
# * -1: assume this device is tablet-mode-incapable
# * 0: rely on heuristics
# * 1: assume this device is tablet-mode-capable
- name: widget.windows.tablet_detection_override
type: RelaxedAtomicInt32
value: 0
mirror: always
# Whether to give explorer.exe a delated nudge to recalculate the fullscreenness
# of a window after maximizing it.
- name: widget.windows.fullscreen_remind_taskbar

View File

@ -22,10 +22,24 @@ interface nsIWindowsUIUtils : nsISupports
void setWindowIconNoData(in mozIDOMWindowProxy aWindow);
/**
* Whether the OS is currently in tablet mode. Always false on
* non-Windows and on versions of Windows before win10
* Whether the OS is currently in Win10's Tablet Mode. Always false on
* versions of Windows other than Win10.
*
* (Win10 tablet mode is sufficiently different from Win11 tablet mode that
* there is no single getter to retrieve whether we're in a generic "tablet
* mode".)
*/
readonly attribute boolean inTabletMode;
readonly attribute boolean inWin10TabletMode;
/**
* Whether the OS is currently in Windows 11's tablet mode. Always false on
* versions of Windows prior to Win11.
*
* (Win11 tablet mode is sufficiently different from Win10 tablet mode that
* there is no single getter to retrieve whether we're in a generic "tablet
* mode".)
*/
readonly attribute boolean inWin11TabletMode;
/**
* Share URL

View File

@ -23,6 +23,7 @@
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Logging.h"
#include "mozilla/LookAndFeel.h"
#include "mozilla/WindowsVersion.h"
#include "nsLookAndFeel.h"
#include "nsStringFwd.h"
#include "nsWindowDbg.h"
@ -212,13 +213,32 @@ static void OnSettingsChange(WPARAM wParam, LPARAM lParam) {
return;
}
// UserInteractionMode, ConvertibleSlateMode, SystemDockMode may cause
// @media(pointer) queries to change, which layout needs to know about
if (lParamString == u"UserInteractionMode"_ns ||
lParamString == u"ConvertibleSlateMode"_ns ||
lParamString == u"SystemDockMode"_ns) {
// UserInteractionMode, ConvertibleSlateMode, and SystemDockMode may cause
// @media(pointer) queries to change, which layout needs to know about.
//
// The former two of those also imply that the current tablet-mode state needs
// to be updated.
if (lParamString == u"UserInteractionMode"_ns) {
// Documentation implies, and testing shows, that this is seen on Win10
// only.
Unused << NS_WARN_IF(mozilla::IsWin11OrLater());
WindowsUIUtils::UpdateInWin10TabletMode();
NotifyThemeChanged(widget::ThemeChangeKind::MediaQueriesOnly);
WindowsUIUtils::UpdateInTabletMode();
return;
}
if (lParamString == u"ConvertibleSlateMode"_ns) {
// Documentation implies, and testing shows, that this is not seen on Win10.
Unused << NS_WARN_IF(!mozilla::IsWin11OrLater());
WindowsUIUtils::UpdateInWin11TabletMode();
NotifyThemeChanged(widget::ThemeChangeKind::MediaQueriesOnly);
return;
}
if (lParamString == u"SystemDockMode"_ns) {
NotifyThemeChanged(widget::ThemeChangeKind::MediaQueriesOnly);
return;
}
}

View File

@ -727,7 +727,7 @@ bool IMEHandler::IsOnScreenKeyboardSupported() {
if (!IsWin11OrLater()) {
// On Windows 10 we require tablet mode, unless the user has set the
// relevant setting to enable the on-screen keyboard in desktop mode.
if (!IsInTabletMode() && !AutoInvokeOnScreenKeyboardInDesktopMode()) {
if (!IsInWin10TabletMode() && !AutoInvokeOnScreenKeyboardInDesktopMode()) {
return false;
}
}
@ -918,8 +918,8 @@ bool IMEHandler::IsKeyboardPresentOnSlate() {
}
// static
bool IMEHandler::IsInTabletMode() {
bool isInTabletMode = WindowsUIUtils::GetInTabletMode();
bool IMEHandler::IsInWin10TabletMode() {
bool isInTabletMode = WindowsUIUtils::GetInWin10TabletMode();
if (isInTabletMode) {
Preferences::SetString(kOskDebugReason, L"IITM: GetInTabletMode=true.");
} else {

View File

@ -223,7 +223,7 @@ class IMEHandler final {
const std::wstring& aNeedle);
static bool NeedOnScreenKeyboard();
static bool IsKeyboardPresentOnSlate();
static bool IsInTabletMode();
static bool IsInWin10TabletMode();
static bool AutoInvokeOnScreenKeyboardInDesktopMode();
static bool NeedsToAssociateIMC();
static bool NeedsSearchInputScope();

View File

@ -1583,9 +1583,8 @@ static bool IsTabletDevice() {
// Guarantees that:
// - The device has a touch screen.
// - It is used as a tablet which means that it has no keyboard connected.
// On Windows 10 it means that it is verifying with ConvertibleSlateMode.
if (WindowsUIUtils::GetInTabletMode()) {
if (WindowsUIUtils::GetInWin10TabletMode()) {
return true;
}

View File

@ -4,7 +4,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <windows.h>
#include <winreg.h>
#include <wrl.h>
#include <powerbase.h>
#include <cfgmgr32.h>
#include "nsServiceManagerUtils.h"
@ -29,6 +32,11 @@
#include "nsPIDOMWindow.h"
#include "nsWindowGfx.h"
#include "Units.h"
#include "nsWindowsHelpers.h"
#include "WinRegistry.h"
#include "WinUtils.h"
mozilla::LazyLogModule gTabletModeLog("TabletMode");
/* mingw currently doesn't support windows.ui.viewmanagement.h, so we disable it
* until it's fixed. */
@ -188,8 +196,10 @@ IUISettings5 : public IInspectable {
using namespace mozilla;
// Since Win10 and Win11 tablet modes can't both be simultaneously active, we
// only need one backing variable for the both of them.
enum class TabletModeState : uint8_t { Unknown, Off, On };
static TabletModeState sInTabletModeState;
static TabletModeState sInTabletModeState = TabletModeState::Unknown;
WindowsUIUtils::WindowsUIUtils() = default;
WindowsUIUtils::~WindowsUIUtils() = default;
@ -282,17 +292,37 @@ WindowsUIUtils::SetWindowIconNoData(mozIDOMWindowProxy* aWindow) {
return NS_OK;
}
bool WindowsUIUtils::GetInTabletMode() {
bool WindowsUIUtils::GetInWin10TabletMode() {
MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
if (IsWin11OrLater()) {
return false;
}
if (sInTabletModeState == TabletModeState::Unknown) {
UpdateInTabletMode();
UpdateInWin10TabletMode();
}
return sInTabletModeState == TabletModeState::On;
}
bool WindowsUIUtils::GetInWin11TabletMode() {
MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
if (!IsWin11OrLater()) {
return false;
}
if (sInTabletModeState == TabletModeState::Unknown) {
UpdateInWin11TabletMode();
}
return sInTabletModeState == TabletModeState::On;
}
NS_IMETHODIMP
WindowsUIUtils::GetInTabletMode(bool* aResult) {
*aResult = GetInTabletMode();
WindowsUIUtils::GetInWin10TabletMode(bool* aResult) {
*aResult = GetInWin10TabletMode();
return NS_OK;
}
NS_IMETHODIMP
WindowsUIUtils::GetInWin11TabletMode(bool* aResult) {
*aResult = GetInWin11TabletMode();
return NS_OK;
}
@ -543,7 +573,18 @@ bool WindowsUIUtils::ComputeTransparencyEffects() {
#endif
}
void WindowsUIUtils::UpdateInTabletMode() {
void WindowsUIUtils::UpdateInWin10TabletMode() {
if (IsWin11OrLater()) {
// (In theory we should never get here under Win11; but it's conceivable
// that there are third-party applications that try to "assist" legacy Win10
// apps by synthesizing Win10-style tablet-mode notifications.)
return;
}
// The getter below relies on querying a HWND which is affine to the main
// thread; its operation is not known to be thread-safe, let alone lock-free.
MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
#ifndef __MINGW32__
nsresult rv;
nsCOMPtr<nsIWindowMediator> winMediator(
@ -592,6 +633,7 @@ void WindowsUIUtils::UpdateInTabletMode() {
TabletModeState oldTabletModeState = sInTabletModeState;
sInTabletModeState = mode == UserInteractionMode_Touch ? TabletModeState::On
: TabletModeState::Off;
if (sInTabletModeState != oldTabletModeState) {
nsCOMPtr<nsIObserverService> observerService =
mozilla::services::GetObserverService();
@ -603,6 +645,199 @@ void WindowsUIUtils::UpdateInTabletMode() {
#endif
}
// Cache: whether this device is believed to be capable of entering tablet mode.
//
// Meaningful only if `IsWin11OrLater()`.
static Maybe<bool> sIsTabletCapable = Nothing();
// The UUID of a GPIO pin which indicates whether or not a convertible device is
// currently in tablet mode. (We copy `DEFINE_GUID`'s implementation here since
// we can't control `INITGUID`, which the canonical one is conditional on.)
//
// https://learn.microsoft.com/en-us/windows-hardware/drivers/gpiobtn/laptop-slate-mode-toggling-between-states
#define MOZ_DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \
EXTERN_C const GUID DECLSPEC_SELECTANY name = { \
l, w1, w2, {b1, b2, b3, b4, b5, b6, b7, b8}}
/* 317fc439-3f77-41c8-b09e-08ad63272aa3 */ MOZ_DEFINE_GUID(
MOZ_GUID_GPIOBUTTONS_LAPTOPSLATE_INTERFACE, 0x317fc439, 0x3f77, 0x41c8,
0xb0, 0x9e, 0x08, 0xad, 0x63, 0x27, 0x2a, 0xa3);
void WindowsUIUtils::UpdateInWin11TabletMode() {
// The OS-level getter itself is threadsafe, but we retain the main-thread
// restriction to parallel the Win10 getter's (presumed) restriction.
MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
if (!IsWin11OrLater()) {
// We should ordinarily never reach this point in Win10 -- but there may
// well be some third-party application out there that synthesizes Win11-
// style tablet-mode notifications.
return;
}
// *** *** *** WARNING: RELIANCE ON UNDOCUMENTED BEHAVIOR *** *** ***
//
// Windows 10's `UserInteractionMode` API is no longer useful under Windows
// 11: it always returns `UserInteractionMode_Mouse`.
//
// The documented API to query whether we're in tablet mode (alt.: "slate
// mode") under Windows 11 is `::GetSystemMetrics(SM_CONVERTIBLESLATEMODE)`.
// This returns 0 if we are in slate mode and 1 otherwise... except on devices
// where tablet mode is unavailable (such as desktops), in which case it
// returns 0 unconditionally.
//
// Unfortunately, there is no documented API to determine whether
// `SM_CONVERTIBLESLATEMODE` is `0` because the device is currently in slate
// mode or because the device can never be in slate mode.
//
// As such, we follow Chromium's lead here, and attempt to determine
// heuristically whether that API is going to return anything sensible.
// (Indeed, the heuristic below is in large part taken from Chromium.)
if (sIsTabletCapable.isNothing()) {
bool const heuristic = ([]() -> bool {
// If the user has set the relevant pref to override our tablet-detection
// heuristics, go with that.
switch (StaticPrefs::widget_windows_tablet_detection_override()) {
case -1:
MOZ_LOG(gTabletModeLog, LogLevel::Info,
("TCH: override detected (-1)"));
return false;
case 1:
MOZ_LOG(gTabletModeLog, LogLevel::Info,
("TCH: override detected (+1)"));
return true;
default:
break;
}
// If ::GSM(SM_CONVERTIBLESLATEMODE) is _currently_ nonzero, we must be on
// a system that does somnething with SM_CONVERTIBLESLATEMODE, so we can
// trust it.
if (::GetSystemMetrics(SM_CONVERTIBLESLATEMODE) != 0) {
MOZ_LOG(gTabletModeLog, LogLevel::Info,
("TCH: SM_CONVERTIBLESLATEMODE != 0"));
return true;
}
// If the device does not support touch it can't possibly be a tablet.
if (GetSystemMetrics(SM_MAXIMUMTOUCHES) == 0) {
MOZ_LOG(gTabletModeLog, LogLevel::Info,
("TCH: SM_MAXIMUMTOUCHES != 0"));
return false;
}
// Check to see if a particular registry key [1] exists. If so, this is
// probably a tablet-capable device.
//
// Comments in Chromium [2] claim that not all devices actually set this
// registry key, but do not actually state that there are _convertible_
// devices which do not. No exceptions are presently known.
//
// [1] https://learn.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/microsoft-windows-gpiobuttons-convertibleslatemode
// [2] https://source.chromium.org/chromium/chromium/src/+/main:base/win/win_util.cc;l=240;drc=5a02fc6cdee77d0a39e9c43a4c2a29bbccc88852
namespace Reg = mozilla::widget::WinRegistry;
Reg::Key key(HKEY_LOCAL_MACHINE,
uR"(System\CurrentControlSet\Control\PriorityControl)"_ns,
Reg::KeyMode::QueryValue);
if (key && key.GetValueType(u"ConvertibleSlateMode"_ns) !=
Reg::ValueType::None) {
MOZ_LOG(gTabletModeLog, LogLevel::Info,
("TCH: 'ConvertibleSlateMode' found"));
return true;
}
// If the device has this GUID mapped to a GPIO pin, it's almost certainly
// tablet-capable. (It's not certain whether the converse is true.)
//
// https://learn.microsoft.com/en-us/windows-hardware/design/device-experiences/continuum#designing-your-device-for-tablet-mode
bool const hasTabletGpioPin = [&]() {
ULONG size = 0;
GUID guid{MOZ_GUID_GPIOBUTTONS_LAPTOPSLATE_INTERFACE};
CONFIGRET const err = ::CM_Get_Device_Interface_List_SizeW(
&size, &guid, nullptr, CM_GET_DEVICE_INTERFACE_LIST_PRESENT);
// (The next step at this point would usually be to call the function
// "::CM_Get_Device_Interface_ListW()" -- but we don't care where the
// associated device interface is actually mapped to; we only care
// whether it's mapped at all.
//
// For our purposes, a zero-length null-terminated string doesn't count
// as "present".)
return err == CR_SUCCESS && size > 1;
}();
if (hasTabletGpioPin) {
MOZ_LOG(gTabletModeLog, LogLevel::Info,
("TCH: relevant GPIO interface found"));
return true;
}
// If the device has no rotation sensor, it's _probably_ not a convertible
// device. (There are exceptions! See bug 1918292.)
AR_STATE rotation_state;
if (HRESULT hr = ::GetAutoRotationState(&rotation_state); !FAILED(hr)) {
if ((rotation_state & (AR_NOT_SUPPORTED | AR_LAPTOP | AR_NOSENSOR)) !=
0) {
MOZ_LOG(gTabletModeLog, LogLevel::Info, ("TCH: no rotation sensor"));
return false;
}
}
// If the device returns `PlatformRoleSlate` for its POWER_PLATFORM_ROLE,
// it's probably tablet capable.
//
// The converse is known to be false; a Dell Inspiron 14 7445 2-in-1
// returns `PlatformRoleMobile`.
//
// (Chromium checks for PlatformRoleMobile as well, but (e.g.) a Dell XPS
// 15 9500 returns `PlatformRoleMobile` despite not being tablet-capable.)
POWER_PLATFORM_ROLE const role =
mozilla::widget::WinUtils::GetPowerPlatformRole();
if (role == PlatformRoleSlate) {
MOZ_LOG(gTabletModeLog, LogLevel::Info,
("TCH: role == PlatformRoleSlate"));
return true;
}
// Without some specific indicator of tablet-capability, assume that we're
// tablet-incapable.
MOZ_LOG(gTabletModeLog, LogLevel::Info,
("TCH: no indication; falling through"));
return false;
})();
MOZ_LOG(gTabletModeLog, LogLevel::Info,
("tablet-capability heuristic: %s", heuristic ? "true" : "false"));
sIsTabletCapable = Some(heuristic);
// If we appear not to be tablet-capable, don't bother doing the check.
// (We also don't need to send a signal.)
if (!heuristic) {
sInTabletModeState = TabletModeState::Off;
return;
}
} else if (sIsTabletCapable == Some(false)) {
// We've been in here before, and the heuristic came back false... but
// somehow, we've just gotten an update for the convertible-slate-mode
// state.
//
// Clearly the heuristic was wrong!
//
// TODO(rkraesig): should we add telemetry to see how often this gets hit?
MOZ_LOG(gTabletModeLog, LogLevel::Warning,
("recv'd update signal after false heuristic run; reversing"));
sIsTabletCapable = Some(true);
}
// at this point, we must be tablet-capable
MOZ_ASSERT(sIsTabletCapable == Some(true));
TabletModeState const oldState = sInTabletModeState;
bool const isTableting =
::GetSystemMetrics(SM_CONVERTIBLESLATEMODE) == 0 /* [sic!] */;
sInTabletModeState = isTableting ? TabletModeState::On : TabletModeState::Off;
}
#ifndef __MINGW32__
struct HStringDeleter {
using pointer = HSTRING;

View File

@ -29,8 +29,21 @@ class WindowsUIUtils final : public nsIWindowsUIUtils {
static RefPtr<SharePromise> Share(nsAutoString aTitle, nsAutoString aText,
nsAutoString aUrl);
static void UpdateInTabletMode();
static bool GetInTabletMode();
static void UpdateInWin10TabletMode();
static void UpdateInWin11TabletMode();
// Check whether we're in Win10 tablet mode.
//
// (Win10 tablet mode is considered sufficiently different from Win11 tablet
// mode that there is no single getter to retrieve whether we're in a generic
// "tablet mode".)
static bool GetInWin10TabletMode();
// Check whether we're in Win11 tablet mode.
//
// (Win11 tablet mode is considered sufficiently different from Win10 tablet
// mode that there is no single getter to retrieve whether we're in a generic
// "tablet mode".)
static bool GetInWin11TabletMode();
// Gets the system accent color, or one of the darker / lighter variants
// (darker = -1/2/3, lighter=+1/2/3, values outside of that range are

View File

@ -73,6 +73,7 @@
#ifdef XP_WIN
# include "mozilla/PreXULSkeletonUI.h"
# include "mozilla/WindowsVersion.h"
# include "nsIWindowsUIUtils.h"
#endif
@ -1818,7 +1819,11 @@ nsresult AppWindow::MaybeSaveEarlyWindowPersistentValues(
nsCOMPtr<nsIWindowsUIUtils> uiUtils(
do_GetService("@mozilla.org/windows-ui-utils;1"));
if (!NS_WARN_IF(!uiUtils)) {
uiUtils->GetInTabletMode(&isInTabletMode);
if (IsWin11OrLater()) {
uiUtils->GetInWin11TabletMode(&isInTabletMode);
} else {
uiUtils->GetInWin10TabletMode(&isInTabletMode);
}
}
}