diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 69bc7a7b3fef..7f4f968dbdcd 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -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 }; diff --git a/browser/components/BrowserContentHandler.sys.mjs b/browser/components/BrowserContentHandler.sys.mjs index 2281642d8f04..cb26e5de9d5d 100644 --- a/browser/components/BrowserContentHandler.sys.mjs +++ b/browser/components/BrowserContentHandler.sys.mjs @@ -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(); diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index fc7f89859381..67a90c3288da 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -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 diff --git a/widget/nsIWindowsUIUtils.idl b/widget/nsIWindowsUIUtils.idl index 6c84e90afd16..e0904fe4a72b 100644 --- a/widget/nsIWindowsUIUtils.idl +++ b/widget/nsIWindowsUIUtils.idl @@ -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 diff --git a/widget/windows/WinEventObserver.cpp b/widget/windows/WinEventObserver.cpp index 58ba89e50ca4..16e383b2db2f 100644 --- a/widget/windows/WinEventObserver.cpp +++ b/widget/windows/WinEventObserver.cpp @@ -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; } } diff --git a/widget/windows/WinIMEHandler.cpp b/widget/windows/WinIMEHandler.cpp index 1313b1eed252..4b08fd79d317 100644 --- a/widget/windows/WinIMEHandler.cpp +++ b/widget/windows/WinIMEHandler.cpp @@ -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 { diff --git a/widget/windows/WinIMEHandler.h b/widget/windows/WinIMEHandler.h index 410e1ebd221f..98f8be96550e 100644 --- a/widget/windows/WinIMEHandler.h +++ b/widget/windows/WinIMEHandler.h @@ -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(); diff --git a/widget/windows/WinUtils.cpp b/widget/windows/WinUtils.cpp index 3f94e338011a..a003aaf6c8bd 100644 --- a/widget/windows/WinUtils.cpp +++ b/widget/windows/WinUtils.cpp @@ -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; } diff --git a/widget/windows/WindowsUIUtils.cpp b/widget/windows/WindowsUIUtils.cpp index 6bc019acaca2..f4ca7d7f792f 100644 --- a/widget/windows/WindowsUIUtils.cpp +++ b/widget/windows/WindowsUIUtils.cpp @@ -4,7 +4,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include +#include #include +#include +#include #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 winMediator( @@ -592,6 +633,7 @@ void WindowsUIUtils::UpdateInTabletMode() { TabletModeState oldTabletModeState = sInTabletModeState; sInTabletModeState = mode == UserInteractionMode_Touch ? TabletModeState::On : TabletModeState::Off; + if (sInTabletModeState != oldTabletModeState) { nsCOMPtr 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 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; diff --git a/widget/windows/WindowsUIUtils.h b/widget/windows/WindowsUIUtils.h index a55f92c8da7f..d6148691d124 100644 --- a/widget/windows/WindowsUIUtils.h +++ b/widget/windows/WindowsUIUtils.h @@ -29,8 +29,21 @@ class WindowsUIUtils final : public nsIWindowsUIUtils { static RefPtr 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 diff --git a/xpfe/appshell/AppWindow.cpp b/xpfe/appshell/AppWindow.cpp index 9e24bd019963..17e938e4138a 100644 --- a/xpfe/appshell/AppWindow.cpp +++ b/xpfe/appshell/AppWindow.cpp @@ -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 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); + } } }